txdb and walletdb work. scanning.

This commit is contained in:
Christopher Jeffrey 2016-05-31 04:37:46 -07:00
parent 74f5a8916f
commit 60d33ce9db
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
4 changed files with 399 additions and 488 deletions

View File

@ -14,11 +14,11 @@
* p/[hash] -> dummy (pending flag) * p/[hash] -> dummy (pending flag)
* m/[time]/[hash] -> dummy (tx by time) * m/[time]/[hash] -> dummy (tx by time)
* h/[height]/[hash] -> dummy (tx by height) * h/[height]/[hash] -> dummy (tx by height)
* T/[id]/[hash] -> dummy (tx by wallet id) * T/[id]/[name]/[hash] -> dummy (tx by wallet id)
* P/[id]/[hash] -> dummy (pending tx by wallet id) * P/[id]/[name]/[hash] -> dummy (pending tx by wallet/account id)
* M/[id]/[time]/[hash] -> dummy (tx by time + id) * M/[id]/[name]/[time]/[hash] -> dummy (tx by time + id/account)
* H/[id]/[height]/[hash] -> dummy (tx by height + id) * H/[id]/[name]/[height]/[hash] -> dummy (tx by height + id/account)
* C/[id]/[hash]/[index] -> dummy (coin by address) * C/[id]/[name]/[hash]/[index] -> dummy (coin by id/account)
*/ */
var bcoin = require('./env'); var bcoin = require('./env');
@ -40,7 +40,8 @@ var BufferWriter = require('./writer');
* @param {Boolean?} options.indexAddress - Index addresses/IDs. * @param {Boolean?} options.indexAddress - Index addresses/IDs.
* @param {Boolean?} options.indexExtra - Index timestamps, heights, etc. * @param {Boolean?} options.indexExtra - Index timestamps, heights, etc.
* @param {Boolean?} options.verify - Verify transactions as they * @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) { function TXDB(db, options) {
@ -52,7 +53,7 @@ function TXDB(db, options) {
if (!options) if (!options)
options = {}; options = {};
this.wdb = db; this.walletdb = db;
this.db = db.db; this.db = db.db;
this.options = options; this.options = options;
this.network = bcoin.network.get(options.network); this.network = bcoin.network.get(options.network);
@ -126,27 +127,8 @@ TXDB.prototype._testFilter = function _testFilter(addresses) {
* @param {Function} callback - Returns [Error, {@link AddressMap}]. * @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) { TXDB.prototype.getMap = function getMap(tx, callback) {
var input, output, addresses, table, map; var i, input, output, address, addresses, table, map;
input = tx.getInputHashes(); input = tx.getInputHashes();
output = tx.getOutputHashes(); output = tx.getOutputHashes();
@ -155,7 +137,7 @@ TXDB.prototype.getMap = function getMap(tx, callback) {
if (!this._testFilter(addresses)) if (!this._testFilter(addresses))
return callback(); return callback();
function cb(err, table) { this.mapAddresses(addresses, function(err, table) {
if (err) if (err)
return callback(err); return callback(err);
@ -169,29 +151,29 @@ TXDB.prototype.getMap = function getMap(tx, callback) {
all: [] all: []
}; };
input.forEach(function(address) { for (i = 0; i < input.length; i++) {
address = input[i];
assert(map.table[address]); assert(map.table[address]);
map.input = map.input.concat(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]); assert(map.table[address]);
map.output = map.output.concat(map.table[address]); map.output = map.output.concat(map.table[address]);
}); }
map.input = uniq(map.input); map.input = uniq(map.input);
map.output = uniq(map.output); map.output = uniq(map.output);
map.all = uniq(map.input.concat(map.output)); map.all = uniq(map.input.concat(map.output));
return callback(null, map); return callback(null, map);
} });
return this.mapAddresses(addresses, cb);
}; };
/** /**
* Map an address to a wallet ID. * Map address hashes to a wallet ID.
* @param {Base58Address|Base58Address[]} address * @param {Hash[]} address - Address hashes.
* @param {Function} callback - Returns [Error, {@link AddressTable}]. * @param {Function} callback - Returns [Error, {@link AddressTable}].
*/ */
@ -201,7 +183,7 @@ TXDB.prototype.mapAddresses = function mapAddresses(address, callback) {
var i, keys, values; var i, keys, values;
return utils.forEachSerial(address, function(address, next) { return utils.forEachSerial(address, function(address, next) {
self.wdb.getAddress(address, function(err, paths) { self.walletdb.getAddress(address, function(err, paths) {
if (err) if (err)
return next(err); return next(err);
@ -389,6 +371,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
// Consume unspent money or add orphans // Consume unspent money or add orphans
utils.forEachSerial(tx.inputs, function(input, next, i) { utils.forEachSerial(tx.inputs, function(input, next, i) {
var key, address; var key, address;
var prevout = input.prevout;
if (tx.isCoinbase()) if (tx.isCoinbase())
return next(); return next();
@ -399,11 +382,11 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
if (!address || !map.table[address].length) if (!address || !map.table[address].length)
return next(); return next();
self.getCoin(input.prevout.hash, input.prevout.index, function(err, coin) { self.getCoin(prevout.hash, prevout.index, function(err, coin) {
if (err) if (err)
return next(err); return next(err);
key = input.prevout.hash + '/' + input.prevout.index; key = prevout.hash + '/' + prevout.index;
if (coin) { if (coin) {
// Add TX to inputs and spend money // Add TX to inputs and spend money
@ -432,21 +415,21 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
input.coin = null; 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) if (err)
return next(err); return next(err);
// Are we double-spending? // Are we double-spending?
// Replace older txs with newer ones. // Replace older txs with newer ones.
if (spentBy) { if (spentBy) {
return self.getTX(input.prevout.hash, function(err, prev) { return self.getTX(prevout.hash, function(err, prev) {
if (err) if (err)
return next(err); return next(err);
if (!prev) if (!prev)
return callback(new Error('Could not find double-spent coin.')); 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 // Skip invalid transactions
if (self.options.verify) { if (self.options.verify) {
@ -577,7 +560,7 @@ TXDB.prototype._add = function add(tx, map, callback, force) {
if (err) if (err)
return callback(err); return callback(err);
self.wdb.sync(tx, map, function(err) { self.walletdb.sync(tx, map, function(err) {
if (err) if (err)
return callback(err); return callback(err);
@ -768,7 +751,7 @@ TXDB.prototype._confirm = function _confirm(tx, map, callback, force) {
if (err) if (err)
return callback(err); return callback(err);
self.wdb.sync(tx, map, function(err) { self.walletdb.sync(tx, map, function(err) {
if (err) if (err)
return callback(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. * 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}[]]. * @param {Function} callback - Returns [Error, {@link Hash}[]].
*/ */
TXDB.prototype.getHistoryHashes = function getHistoryHashes(address, callback) { TXDB.prototype.getHistoryHashes = function getHistoryHashes(id, callback) {
var self = this; if (typeof id === 'function') {
var txs = []; callback = id;
id = null;
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);
});
} }
this.db.iterate({ this.db.iterate({
gte: address ? 'T/' + address + '/' : 't', gte: id ? 'T/' + id + '/' : 't',
lte: address ? 'T/' + address + '/~' : 't~', lte: id ? 'T/' + id + '/~' : 't~',
transform: function(key) { transform: function(key) {
key = key.split('/'); key = key.split('/');
if (address) if (id)
return key[3]; return key[3];
return key[1]; return key[1];
} }
@ -1119,48 +1077,22 @@ TXDB.prototype.getHistoryHashes = function getHistoryHashes(address, callback) {
/** /**
* Get hashes of all unconfirmed transactions in the database. * 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}[]]. * @param {Function} callback - Returns [Error, {@link Hash}[]].
*/ */
TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(address, callback) { TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(id, callback) {
var self = this; if (typeof id === 'function') {
var txs = []; callback = id;
id = null;
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);
});
} }
this.db.iterate({ this.db.iterate({
gte: address ? 'P/' + address + '/' : 'p', gte: id ? 'P/' + id + '/' : 'p',
lte: address ? 'P/' + address + '/~' : 'p~', lte: id ? 'P/' + id + '/~' : 'p~',
transform: function(key) { transform: function(key) {
key = key.split('/'); key = key.split('/');
if (address) if (id)
return key[3]; return key[3];
return key[1]; return key[1];
} }
@ -1169,45 +1101,22 @@ TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(address, cal
/** /**
* Get all coin hashes in the database. * 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}[]]. * @param {Function} callback - Returns [Error, {@link Hash}[]].
*/ */
TXDB.prototype.getCoinHashes = function getCoinHashes(address, callback) { TXDB.prototype.getCoinHashes = function getCoinHashes(id, callback) {
var self = this; if (typeof id === 'function') {
var coins = []; callback = id;
id = null;
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);
});
} }
this.db.iterate({ this.db.iterate({
gte: address ? 'C/' + address + '/' : 'c', gte: id ? 'C/' + id + '/' : 'c',
lte: address ? 'C/' + address + '/~' : 'c~', lte: id ? 'C/' + id + '/~' : 'c~',
transform: function(key) { transform: function(key) {
key = key.split('/'); key = key.split('/');
if (address) if (id)
return [key[3], +key[4]]; return [key[3], +key[4]];
return [key[1], +key[2]]; return [key[1], +key[2]];
} }
@ -1216,7 +1125,7 @@ TXDB.prototype.getCoinHashes = function getCoinHashes(address, callback) {
/** /**
* Get TX hashes by height range. * Get TX hashes by height range.
* @param {WalletID|WalletID[]} address - By address (can be null). * @param {WalletID?} id
* @param {Object} options * @param {Object} options
* @param {Number} options.start - Start height. * @param {Number} options.start - Start height.
* @param {Number} options.end - End 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}[]]. * @param {Function} callback - Returns [Error, {@link Hash}[]].
*/ */
TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(address, options, callback) { TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(id, options, callback) {
if (typeof address !== 'string') { if (typeof id !== 'string') {
callback = options; callback = options;
options = address; options = id;
address = null; id = null;
} }
callback = utils.ensure(callback);
this.db.iterate({ this.db.iterate({
gte: address gte: id
? 'H/' + address + '/' + pad32(options.start) + '/' ? 'H/' + id + '/' + pad32(options.start) + '/'
: 'h/' + pad32(options.start) + '/', : 'h/' + pad32(options.start) + '/',
lte: address lte: id
? 'H/' + address + '/' + pad32(options.end) + '/~' ? 'H/' + id + '/' + pad32(options.end) + '/~'
: 'h/' + pad32(options.end) + '/~', : 'h/' + pad32(options.end) + '/~',
limit: options.limit, limit: options.limit,
reverse: options.reverse, reverse: options.reverse,
transform: function(key) { transform: function(key) {
key = key.split('/'); key = key.split('/');
if (address) if (id)
return key[4]; return key[4];
return key[2]; return key[2];
} }
@ -1264,7 +1171,7 @@ TXDB.prototype.getHeightHashes = function getHeightHashes(height, callback) {
/** /**
* Get TX hashes by timestamp range. * Get TX hashes by timestamp range.
* @param {WalletID|WalletID[]} address - By address (can be null). * @param {WalletID?} id
* @param {Object} options * @param {Object} options
* @param {Number} options.start - Start height. * @param {Number} options.start - Start height.
* @param {Number} options.end - End 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}[]]. * @param {Function} callback - Returns [Error, {@link Hash}[]].
*/ */
TXDB.prototype.getRangeHashes = function getRangeHashes(address, options, callback) { TXDB.prototype.getRangeHashes = function getRangeHashes(id, options, callback) {
if (typeof address === 'function') { if (typeof id === 'function') {
callback = address; callback = id;
address = null; id = null;
} }
callback = utils.ensure(callback);
this.db.iterate({ this.db.iterate({
gte: address gte: id
? 'M/' + address + '/' + pad32(options.start) + '/' ? 'M/' + id + '/' + pad32(options.start) + '/'
: 'm/' + pad32(options.start) + '/', : 'm/' + pad32(options.start) + '/',
lte: address lte: id
? 'M/' + address + '/' + pad32(options.end) + '/~' ? 'M/' + id + '/' + pad32(options.end) + '/~'
: 'm/' + pad32(options.end) + '/~', : 'm/' + pad32(options.end) + '/~',
limit: options.limit, limit: options.limit,
reverse: options.reverse, reverse: options.reverse,
transform: function(key) { transform: function(key) {
key = key.split('/'); key = key.split('/');
if (address) if (id)
return key[4]; return key[4];
return key[2]; return key[2];
} }
@ -1301,7 +1206,7 @@ TXDB.prototype.getRangeHashes = function getRangeHashes(address, options, callba
/** /**
* Get transactions by timestamp range. * Get transactions by timestamp range.
* @param {WalletID|WalletID[]} address - By address (can be null). * @param {WalletID?} id
* @param {Object} options * @param {Object} options
* @param {Number} options.start - Start height. * @param {Number} options.start - Start height.
* @param {Number} options.end - End 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}[]]. * @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 self = this;
var txs = []; var txs = [];
if (typeof address === 'function') { if (typeof id === 'function') {
callback = address; callback = id;
address = null; id = null;
} }
return this.getRangeHashes(address, options, function(err, hashes) { return this.getRangeHashes(id, options, function(err, hashes) {
if (err) if (err)
return callback(err); return callback(err);
@ -1346,19 +1251,19 @@ TXDB.prototype.getRange = function getLast(address, options, callback) {
/** /**
* Get last N transactions. * Get last N transactions.
* @param {WalletID|WalletID[]} address - By address (can be null). * @param {WalletID?} id
* @param {Number} limit - Max number of transactions. * @param {Number} limit - Max number of transactions.
* @param {Function} callback - Returns [Error, {@link TX}[]]. * @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') { if (typeof limit === 'function') {
callback = limit; callback = limit;
limit = address; limit = id;
address = null; id = null;
} }
return this.getRange(address, { return this.getRange(id, {
start: 0, start: 0,
end: 0xffffffff, end: 0xffffffff,
reverse: true, reverse: true,
@ -1368,20 +1273,20 @@ TXDB.prototype.getLast = function getLast(address, limit, callback) {
/** /**
* Get all transactions. * Get all transactions.
* @param {WalletID|WalletID[]} address - By address (can be null). * @param {WalletID?} id
* @param {Function} callback - Returns [Error, {@link TX}[]]. * @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 self = this;
var txs = []; var txs = [];
if (typeof address === 'function') { if (typeof id === 'function') {
callback = address; callback = id;
address = null; id = null;
} }
return this.getHistoryHashes(address, function(err, hashes) { return this.getHistoryHashes(id, function(err, hashes) {
if (err) if (err)
return callback(err); return callback(err);
@ -1408,32 +1313,34 @@ TXDB.prototype.getHistory = function getHistory(address, callback) {
/** /**
* Get last active timestamp and height. * 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)]. * @param {Function} callback - Returns [Error, Number(ts), Number(height)].
*/ */
TXDB.prototype.getLastTime = function getLastTime(address, callback) { TXDB.prototype.getLastTime = function getLastTime(id, callback) {
if (typeof address === 'function') { var i, tx, lastTs, lastHeight;
callback = address;
address = null; if (typeof id === 'function') {
callback = id;
id = null;
} }
return this.getHistory(address, function(err, txs) { return this.getHistory(id, function(err, txs) {
var lastTs, lastHeight;
if (err) if (err)
return callback(err); return callback(err);
lastTs = 0; lastTs = 0;
lastHeight = -1; lastHeight = -1;
txs.forEach(function(tx) { for (i = 0; i < txs.length; i++) {
tx = txs[i];
if (tx.ts > lastTs) if (tx.ts > lastTs)
lastTs = tx.ts; lastTs = tx.ts;
if (tx.height > lastHeight) if (tx.height > lastHeight)
lastHeight = tx.height; lastHeight = tx.height;
}); }
return callback(null, lastTs, lastHeight); return callback(null, lastTs, lastHeight);
}); });
@ -1441,20 +1348,20 @@ TXDB.prototype.getLastTime = function getLastTime(address, callback) {
/** /**
* Get unconfirmed transactions. * Get unconfirmed transactions.
* @param {WalletID|WalletID[]} address - By address (can be null). * @param {WalletID?} id
* @param {Function} callback - Returns [Error, {@link TX}[]]. * @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 self = this;
var txs = []; var txs = [];
if (typeof address === 'function') { if (typeof id === 'function') {
callback = address; callback = id;
address = null; id = null;
} }
return this.getUnconfirmedHashes(address, function(err, hashes) { return this.getUnconfirmedHashes(id, function(err, hashes) {
if (err) if (err)
return callback(err); return callback(err);
@ -1481,20 +1388,20 @@ TXDB.prototype.getUnconfirmed = function getUnconfirmed(address, callback) {
/** /**
* Get coins. * Get coins.
* @param {WalletID|WalletID[]} address - By address (can be null). * @param {WalletID?} id
* @param {Function} callback - Returns [Error, {@link Coin}[]]. * @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 self = this;
var coins = []; var coins = [];
if (typeof address === 'function') { if (typeof id === 'function') {
callback = address; callback = id;
address = null; id = null;
} }
return this.getCoinHashes(address, function(err, hashes) { return this.getCoinHashes(id, function(err, hashes) {
if (err) if (err)
return callback(err); return callback(err);
@ -1629,8 +1536,8 @@ TXDB.prototype.hasTX = function hasTX(hash, callback) {
*/ */
TXDB.prototype.getCoin = function getCoin(hash, index, callback) { TXDB.prototype.getCoin = function getCoin(hash, index, callback) {
this.db.fetch('c/' + hash + '/' + index, function(coin) { this.db.fetch('c/' + hash + '/' + index, function(data) {
coin = bcoin.coin.fromRaw(coin); var coin = bcoin.coin.fromRaw(data);
coin.hash = hash; coin.hash = hash;
coin.index = index; coin.index = index;
return coin; return coin;
@ -1649,39 +1556,21 @@ TXDB.prototype.hasCoin = function hasCoin(hash, index, callback) {
/** /**
* Calculate balance. * Calculate balance.
* @param {WalletID|WalletID[]} address - By address (can be null). * @param {WalletID?} id
* @param {Function} callback - Returns [Error, {@link Balance}]. * @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 self = this;
var confirmed = 0; var confirmed = 0;
var unconfirmed = 0; var unconfirmed = 0;
if (typeof address === 'function') { if (typeof id === 'function') {
callback = address; callback = id;
address = null; id = null;
} }
// return this.getCoins(address, function(err, coins) { return this.getCoinHashes(id, function(err, hashes) {
// 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) {
if (err) if (err)
return callback(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 {Number} age - Age delta (delete transactions older than `now - age`).
* @param {Function} callback * @param {Function} callback
*/ */
TXDB.prototype.zap = function zap(address, age, callback, force) { TXDB.prototype.zap = function zap(id, age, callback, force) {
var self = this; var self = this;
if (typeof address !== 'string') { if (typeof age === 'function') {
force = callback; force = callback;
callback = age; callback = age;
age = address; age = id;
address = null; id = null;
} }
var unlock = this._lock(zap, [address, age, callback], force); var unlock = this._lock(zap, [id, age, callback], force);
if (!unlock) if (!unlock)
return; return;
callback = utils.wrap(callback, unlock); 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, start: 0,
end: bcoin.now() - age end: bcoin.now() - age
}, function(err, txs) { }, function(err, txs) {
@ -1748,12 +1638,35 @@ TXDB.prototype.zap = function zap(address, age, callback, force) {
utils.forEachSerial(txs, function(tx, next) { utils.forEachSerial(txs, function(tx, next) {
if (tx.ts !== 0) if (tx.ts !== 0)
return next(); return next();
self.lazyRemove(tx, next); self.lazyRemove(tx, next, true);
}, callback); }, 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 * Expose
*/ */

View File

@ -2277,14 +2277,14 @@ utils.parallel = function parallel(stack, callback) {
for (i = 0; i < stack.length; i++) { for (i = 0; i < stack.length; i++) {
try { try {
if (0 && stack[i].length >= 2) { // if (stack[i].length >= 2) {
stack[i](error, next); // stack[i](error, next);
error = null; // error = null;
} else { // continue;
if (error) // }
continue; if (error)
stack[i](next); continue;
} stack[i](next);
} catch (e) { } catch (e) {
pending--; pending--;
error = e; error = e;
@ -2306,14 +2306,13 @@ utils.serial = function serial(stack, callback) {
if (!cb) if (!cb)
return callback(err); return callback(err);
if (0) // if (cb.length >= 2) {
if (cb.length >= 2) { // try {
try { // return cb(err, next);
return cb(err, next); // } catch (e) {
} catch (e) { // return next(e);
return next(e); // }
} // }
}
if (err) if (err)
return utils.nextTick(next.bind(null, err)); return utils.nextTick(next.bind(null, err));

View File

@ -76,6 +76,7 @@ function Wallet(options) {
if (!this.id) if (!this.id)
this.id = this.getID(); this.id = this.getID();
// An in-memory database for testing.
if (!this.db) { if (!this.db) {
this.db = new bcoin.walletdb({ this.db = new bcoin.walletdb({
network: this.network, network: this.network,
@ -83,9 +84,6 @@ function Wallet(options) {
db: 'memory' 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); utils.inherits(Wallet, EventEmitter);
@ -214,6 +212,7 @@ Wallet.prototype.addKey = function addKey(account, key, callback) {
key = account; key = account;
account = 0; account = 0;
} }
this.getAccount(account, function(err, account) { this.getAccount(account, function(err, account) {
if (err) if (err)
return callback(err); return callback(err);
@ -238,6 +237,7 @@ Wallet.prototype.removeKey = function removeKey(account, key, callback) {
key = account; key = account;
account = 0; account = 0;
} }
this.getAccount(account, function(err, account) { this.getAccount(account, function(err, account) {
if (err) if (err)
return callback(err); return callback(err);
@ -294,7 +294,7 @@ Wallet.prototype.createAccount = function createAccount(options, callback) {
options = { options = {
network: this.network, network: this.network,
wid: this.id, id: this.id,
name: this.accountDepth === 0 ? 'default' : options.name, name: this.accountDepth === 0 ? 'default' : options.name,
witness: options.witness, witness: options.witness,
accountKey: key.hdPublicKey, accountKey: key.hdPublicKey,
@ -854,31 +854,31 @@ Wallet.prototype.zap = function zap(account, age, callback) {
}; };
/** /**
* Scan for addresses. * Scan for active accounts and addresses. Used for importing a wallet.
* @param {Function} getByAddress - Must be a callback which accepts * @param {Function} getByAddress - Must be a function which accepts
* a callback and returns transactions by address. * a {@link Base58Address} as well as a callback and returns
* @param {Function} callback - Returns [Error, Boolean, TX[]]. * transactions by address.
* @param {Function} callback - Return [Error, Number] (total number
* of addresses allocated).
*/ */
Wallet.prototype.scan = function scan(getByAddress, callback) { Wallet.prototype.scan = function scan(getByAddress, callback) {
var self = this; var self = this;
var result = false; var total = 0;
var txs = [];
(function next() { (function next() {
self.createAccount(self.options, function(err, account) { self.createAccount(self.options, function(err, account) {
if (err) if (err)
return callback(err); return callback(err);
account.scan(getByAddress, function(err, res, tx) { account.scan(getByAddress, function(err, result) {
if (err) if (err)
return callback(err); return callback(err);
if (!res) if (result === 0)
return callback(null, result, txs); return callback(null, total);
result = true; total += result;
txs = txs.concat(tx);
next(); next();
}); });
@ -978,6 +978,7 @@ Wallet.prototype.addTX = function addTX(tx, callback) {
/** /**
* Get all transactions in transaction history (accesses db). * Get all transactions in transaction history (accesses db).
* @param {(String|Number)?} account
* @param {Function} callback - Returns [Error, {@link TX}[]]. * @param {Function} callback - Returns [Error, {@link TX}[]].
*/ */
@ -1444,8 +1445,8 @@ Wallet.fromJSON = function fromJSON(json) {
Wallet.isWallet = function isWallet(obj) { Wallet.isWallet = function isWallet(obj) {
return obj return obj
&& typeof obj.receiveDepth === 'number' && typeof obj.accountDepth === 'number'
&& obj.deriveAddress === 'function'; && obj.scriptInputs === 'function';
}; };
/** /**
@ -1454,11 +1455,9 @@ Wallet.isWallet = function isWallet(obj) {
* @constructor * @constructor
* @param {Object} options * @param {Object} options
* @param {WalletDB} options.db * @param {WalletDB} options.db
* present, no coins will be available. * @param {HDPublicKey} options.accountKey
* @param {(HDPrivateKey|HDPublicKey)?} options.master - Master HD key. If not
* present, it will be generated.
* @param {Boolean?} options.witness - Whether to use witness programs. * @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 * @param {Number?} options.receiveDepth - The index of the _next_ receiving
* address. * address.
* @param {Number?} options.changeDepth - The index of the _next_ change * @param {Number?} options.changeDepth - The index of the _next_ change
@ -1467,8 +1466,8 @@ Wallet.isWallet = function isWallet(obj) {
* (default=pubkeyhash). * (default=pubkeyhash).
* @param {Number?} options.m - `m` value for multisig. * @param {Number?} options.m - `m` value for multisig.
* @param {Number?} options.n - `n` value for multisig. * @param {Number?} options.n - `n` value for multisig.
* @param {String?} options.wid - Account ID (used for storage) * @param {String?} options.id - Wallet ID
* (default=account key "address"). * @param {String?} options.name - Account name
*/ */
function Account(options) { function Account(options) {
@ -1481,19 +1480,20 @@ function Account(options) {
assert(options, 'Options are required.'); assert(options, 'Options are required.');
assert(options.db, 'Database is 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(options.accountKey, 'Account key is required.');
assert(utils.isNumber(options.accountIndex), 'Account index is required.');
this.options = options; this.options = options;
this.network = bcoin.network.get(options.network); this.network = bcoin.network.get(options.network);
this.db = options.db; this.db = options.db;
this.lookahead = options.lookahead != null ? options.lookahead : 5; this.lookahead = options.lookahead != null ? options.lookahead : 5;
this.wid = options.wid; this.id = options.id;
this.name = options.name; this.name = options.name;
this.witness = options.witness || false; this.witness = options.witness || false;
this.accountKey = options.accountKey; this.accountKey = options.accountKey;
this.accountIndex = options.accountIndex || 0; this.accountIndex = options.accountIndex;
this.receiveDepth = options.receiveDepth || 1; this.receiveDepth = options.receiveDepth || 1;
this.changeDepth = options.changeDepth || 1; this.changeDepth = options.changeDepth || 1;
this.type = options.type || 'pubkeyhash'; this.type = options.type || 'pubkeyhash';
@ -1949,7 +1949,7 @@ Account.prototype.save = function save(callback) {
*/ */
Account.prototype.saveAddress = function saveAddress(address, 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. * Scan for addresses.
* @param {Function} getByAddress - Must be a callback which accepts * @param {Function} getByAddress - Must be a callback which accepts
* a callback and returns transactions by address. * 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) { Account.prototype.scan = function scan(getByAddress, callback) {
var self = this; var self = this;
var result = false; var total = 0;
return this._scan(getByAddress, function(err, depth, txs) { if (!this.initialized)
if (err) return callback(new Error('Account is uninitialized.'));
return callback(err);
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) if (err)
return callback(err); return callback(err);
if (res) return callback(null, true);
result = true;
self.setReceiveDepth(depth.receiveDepth + 1, function(err, res) {
if (err)
return callback(err);
if (res)
result = true;
return callback(null, result, txs);
});
}); });
}); }
};
Account.prototype._scan = function _scan(getByAddress, callback) {
var self = this;
var depth = { changeDepth: 0, receiveDepth: 0 };
var all = [];
assert(this.initialized);
(function chainCheck(change) { (function chainCheck(change) {
var addressIndex = 0;
var total = 0;
var gap = 0; var gap = 0;
(function next() { (function next() {
var address = self.deriveAddress(change, addressIndex++); self.createAddress(change, function(err, address) {
getByAddress(address.getAddress(), function(err, txs) {
var result;
if (err) if (err)
return callback(err); return callback(err);
if (txs) { getByAddress(address.getAddress(), function(err, txs) {
if (typeof txs === 'boolean') if (err)
result = txs; return callback(err);
else if (typeof txs === 'number')
result = txs > 0;
else if (Array.isArray(txs))
result = txs.length > 0;
else
result = false;
if (Array.isArray(txs) && (txs[0] instanceof bcoin.tx)) addTX(txs, function(err, result) {
all = all.concat(txs); if (err)
} return callback(err);
if (result) { if (result) {
total++; total++;
gap = 0; gap = 0;
return next(); return next();
} }
if (++gap < 20) if (++gap < 20)
return next(); return next();
assert(depth.receiveDepth === 0 || change === true); if (!change)
return chainCheck(true);
if (change === false) return callback(null, total);
depth.receiveDepth = addressIndex - gap; });
else });
depth.changeDepth = addressIndex - gap;
if (change === false)
return chainCheck(true);
return callback(null, depth, all);
}); });
})(); })();
})(false); })(false);
@ -2130,9 +2100,9 @@ Account.prototype._scan = function _scan(getByAddress, callback) {
Account.prototype.inspect = function inspect() { Account.prototype.inspect = function inspect() {
return { return {
wid: this.wid, id: this.id,
name: this.name, name: this.name,
network: this.network.type, network: this.network,
initialized: this.initialized, initialized: this.initialized,
type: this.type, type: this.type,
m: this.m, m: this.m,
@ -2167,7 +2137,7 @@ Account.prototype.inspect = function inspect() {
Account.prototype.toJSON = function toJSON() { Account.prototype.toJSON = function toJSON() {
return { return {
network: this.network.type, network: this.network.type,
wid: this.wid, id: this.id,
name: this.name, name: this.name,
initialized: this.initialized, initialized: this.initialized,
type: this.type, type: this.type,
@ -2198,7 +2168,7 @@ Account.prototype.toJSON = function toJSON() {
Account.parseJSON = function parseJSON(json) { Account.parseJSON = function parseJSON(json) {
return { return {
network: json.network, network: json.network,
wid: json.wid, id: json.id,
name: json.name, name: json.name,
initialized: json.initialized, initialized: json.initialized,
type: json.type, type: json.type,
@ -2225,7 +2195,7 @@ Account.prototype.toRaw = function toRaw(writer) {
var i; var i;
p.writeU32(this.network.magic); p.writeU32(this.network.magic);
p.writeVarString(this.wid, 'utf8'); p.writeVarString(this.id, 'utf8');
p.writeVarString(this.name, 'utf8'); p.writeVarString(this.name, 'utf8');
p.writeU8(this.initialized ? 1 : 0); p.writeU8(this.initialized ? 1 : 0);
p.writeU8(this.type === 'pubkeyhash' ? 0 : 1); p.writeU8(this.type === 'pubkeyhash' ? 0 : 1);
@ -2258,7 +2228,7 @@ Account.prototype.toRaw = function toRaw(writer) {
Account.parseRaw = function parseRaw(data) { Account.parseRaw = function parseRaw(data) {
var p = new BufferReader(data); var p = new BufferReader(data);
var network = bcoin.network.fromMagic(p.readU32()); var network = bcoin.network.fromMagic(p.readU32());
var wid = p.readVarString('utf8'); var id = p.readVarString('utf8');
var name = p.readVarString('utf8'); var name = p.readVarString('utf8');
var initialized = p.readU8() === 1; var initialized = p.readU8() === 1;
var type = p.readU8() === 0 ? 'pubkeyhash' : 'multisig'; var type = p.readU8() === 0 ? 'pubkeyhash' : 'multisig';
@ -2277,7 +2247,7 @@ Account.parseRaw = function parseRaw(data) {
return { return {
network: network.type, network: network.type,
wid: wid, id: id,
name: name, name: name,
initialized: initialized, initialized: initialized,
type: type, type: type,

View File

@ -10,6 +10,8 @@
* (inherits all from txdb) * (inherits all from txdb)
* W/[address] -> id & path data * W/[address] -> id & path data
* w/[id] -> wallet * w/[id] -> wallet
* a/[id]/[index] -> account
* i/[id]/[name] -> account index
*/ */
var bcoin = require('./env'); var bcoin = require('./env');
@ -346,140 +348,12 @@ WalletDB.prototype.get = function get(id, callback) {
*/ */
WalletDB.prototype.save = function save(wallet, 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); 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. * Create a new wallet, save to database, setup watcher.
* @param {Object} options - See {@link Wallet}. * @param {Object} options - See {@link Wallet}.
@ -502,9 +376,6 @@ WalletDB.prototype.create = function create(options, callback) {
options = utils.merge({}, options); options = utils.merge({}, options);
if (self.network.witness)
options.witness = options.witness !== false;
options.network = self.network; options.network = self.network;
options.db = self; options.db = self;
wallet = new bcoin.wallet(options); 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. * Save an address to the path map.
* The path map exists in the form of: * The path map exists in the form of:
@ -958,6 +983,10 @@ function serializePaths(out) {
return p.render(); return p.render();
} }
function isAlpha(key) {
return /^[a-zA-Z0-9]+$/.test(key);
}
/* /*
* Expose * Expose
*/ */