wallet: switch to using number ids.

This commit is contained in:
Christopher Jeffrey 2016-08-15 03:51:51 -07:00
parent c147e5bdf3
commit 9e2dd9145f
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
6 changed files with 355 additions and 226 deletions

View File

@ -41,7 +41,8 @@ function KeyRing(options) {
this.m = 1;
this.n = 1;
this.witness = false;
this.id = null;
this.id = 0;
this.label = null;
this.name = null;
this.account = 0;
this.change = 0;
@ -99,10 +100,15 @@ KeyRing.prototype.fromOptions = function fromOptions(options) {
}
if (options.id) {
assert(utils.isAlpha(options.id));
assert(utils.isNumber(options.id));
this.id = options.id;
}
if (options.label) {
assert(utils.isAlpha(options.label));
this.label = options.label;
}
if (options.name) {
assert(utils.isAlpha(options.name));
this.name = options.name;
@ -656,6 +662,7 @@ KeyRing.prototype.toJSON = function toJSON() {
n: this.n,
witness: this.witness,
id: this.id,
label: this.label,
name: this.name,
account: this.account,
change: this.change,
@ -683,7 +690,8 @@ KeyRing.prototype.fromJSON = function fromJSON(json) {
assert(utils.isNumber(json.m));
assert(utils.isNumber(json.n));
assert(typeof json.witness === 'boolean');
assert(!json.id || utils.isAlpha(json.id));
assert(!json.id || utils.isNumber(json.id));
assert(!json.label || utils.isAlpha(json.label));
assert(!json.name || utils.isAlpha(json.name));
assert(utils.isNumber(json.account));
assert(utils.isNumber(json.change));
@ -733,7 +741,8 @@ KeyRing.prototype.toRaw = function toRaw(writer) {
p.writeU8(this.m);
p.writeU8(this.n);
p.writeU8(this.witness ? 1 : 0);
p.writeVarString(this.id, 'utf8');
p.writeU32(this.id);
p.writeVarString(this.label, 'utf8');
p.writeVarString(this.name, 'utf8');
p.writeU32(this.account);
p.writeU32(this.change);
@ -765,7 +774,8 @@ KeyRing.prototype.fromRaw = function fromRaw(data) {
this.m = p.readU8();
this.n = p.readU8();
this.witness = p.readU8() === 1;
this.id = p.readVarString('utf8');
this.id = p.readU32();
this.label = p.readVarString('utf8');
this.name = p.readVarString('utf8');
this.account = p.readU32();
this.change = p.readU32();

View File

@ -611,6 +611,15 @@ exports.MAX_HASH = new Buffer(
exports.NULL_HASH =
'0000000000000000000000000000000000000000000000000000000000000000';
/**
* A hash of all 0xff.
* @const {String}
* @default
*/
exports.HIGH_HASH =
'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
/**
* A hash of all zeroes.
* @const {Buffer}

View File

@ -27,6 +27,7 @@
var bcoin = require('./env');
var utils = require('./utils');
var assert = bcoin.utils.assert;
var constants = bcoin.protocol.constants;
var DUMMY = new Buffer([0]);
var pad32 = utils.pad32;
var BufferReader = require('./reader');
@ -89,7 +90,7 @@ TXDB.prototype.open = function open(callback) {
TXDB.prototype.prefix = function prefix(key) {
assert(this.wallet.id);
return 't/' + this.wallet.id + '/' + key;
return 't/' + pad32(this.wallet.id) + '/' + key;
};
/**
@ -234,7 +235,7 @@ TXDB.prototype.commit = function commit(callback) {
*/
TXDB.prototype.getInfo = function getInfo(tx, callback) {
this.walletdb.getPathInfo(this.wallet.id, tx, callback);
this.walletdb.getPathInfo(this.wallet, tx, callback);
};
/**
@ -415,7 +416,7 @@ TXDB.prototype._verify = function _verify(tx, info, callback) {
TXDB.prototype._resolveOrphans = function _resolveOrphans(tx, index, callback) {
var self = this;
var hash = tx.hash('hex');
var key = hash + '/' + index;
var key = hash + '/' + pad32(index);
var coin;
this._getOrphans(key, function(err, orphans) {
@ -446,7 +447,7 @@ TXDB.prototype._resolveOrphans = function _resolveOrphans(tx, index, callback) {
// Verify that input script is correct, if not - add
// output to unspent and remove orphan from storage
if (!self.options.verify || orphan.verifyInput(input.index)) {
self.put('d/' + input.hash + '/' + input.index, coin.toRaw());
self.put('d/' + input.hash + '/' + pad32(input.index), coin.toRaw());
return callback(null, true);
}
@ -514,7 +515,7 @@ TXDB.prototype.add = function add(tx, info, callback) {
self.put('m/' + pad32(tx.ps) + '/' + hash, DUMMY);
for (i = 0; i < info.accounts.length; i++) {
id = info.accounts[i];
id = pad32(info.accounts[i]);
self.put('T/' + id + '/' + hash, DUMMY);
if (tx.ts === 0)
self.put('P/' + id + '/' + hash, DUMMY);
@ -538,7 +539,7 @@ TXDB.prototype.add = function add(tx, info, callback) {
if (!path)
return next();
key = prevout.hash + '/' + prevout.index;
key = prevout.hash + '/' + pad32(prevout.index);
// s/[outpoint-key] -> [spender-hash]|[spender-input-index]
outpoint = bcoin.outpoint.fromTX(tx, i).toRaw();
@ -549,8 +550,8 @@ TXDB.prototype.add = function add(tx, info, callback) {
return self._addOrphan(key, outpoint, next);
self.del('c/' + key);
self.del('C/' + path.account + '/' + key);
self.put('d/' + hash + '/' + i, input.coin.toRaw());
self.del('C/' + pad32(path.account) + '/' + key);
self.put('d/' + hash + '/' + pad32(i), input.coin.toRaw());
self.balance.sub(input.coin);
self.coinCache.remove(key);
@ -565,7 +566,7 @@ TXDB.prototype.add = function add(tx, info, callback) {
// Add unspent outputs or resolve orphans
utils.forEachSerial(tx.outputs, function(output, next, i) {
var address = output.getHash('hex');
var key = hash + '/' + i;
var key = hash + '/' + pad32(i);
var coin;
path = info.getPath(address);
@ -586,7 +587,7 @@ TXDB.prototype.add = function add(tx, info, callback) {
coin = coin.toRaw();
self.put('c/' + key, coin);
self.put('C/' + path.account + '/' + key, DUMMY);
self.put('C/' + pad32(path.account) + '/' + key, DUMMY);
self.coinCache.set(key, coin);
@ -752,7 +753,7 @@ TXDB.prototype.isDoubleSpend = function isDoubleSpend(tx, callback) {
*/
TXDB.prototype.isSpent = function isSpent(hash, index, callback) {
var key = 's/' + hash + '/' + index;
var key = 's/' + hash + '/' + pad32(index);
this.fetch(key, function(data) {
return bcoin.outpoint.fromRaw(data);
}, callback);
@ -805,14 +806,14 @@ TXDB.prototype._confirm = function _confirm(tx, info, callback) {
self.put('h/' + pad32(tx.height) + '/' + hash, DUMMY);
for (i = 0; i < info.accounts.length; i++) {
id = info.accounts[i];
id = pad32(info.accounts[i]);
self.del('P/' + id + '/' + hash);
self.put('H/' + id + '/' + pad32(tx.height) + '/' + hash, DUMMY);
}
utils.forEachSerial(tx.outputs, function(output, next, i) {
var address = output.getHash('hex');
var key = hash + '/' + i;
var key = hash + '/' + pad32(i);
// Only update coins if this output is ours.
if (!info.hasPath(address))
@ -923,7 +924,7 @@ TXDB.prototype._remove = function remove(tx, info, callback) {
this.del('m/' + pad32(tx.ps) + '/' + hash);
for (i = 0; i < info.accounts.length; i++) {
id = info.accounts[i];
id = pad32(info.accounts[i]);
this.del('T/' + id + '/' + hash);
if (tx.ts === 0)
this.del('P/' + id + '/' + hash);
@ -938,7 +939,7 @@ TXDB.prototype._remove = function remove(tx, info, callback) {
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
key = input.prevout.hash + '/' + input.prevout.index;
key = input.prevout.hash + '/' + pad32(input.prevout.index);
address = input.getHash('hex');
if (tx.isCoinbase())
@ -957,8 +958,8 @@ TXDB.prototype._remove = function remove(tx, info, callback) {
coin = input.coin.toRaw();
self.put('c/' + key, coin);
self.put('C/' + path.account + '/' + key, DUMMY);
self.del('d/' + hash + '/' + i);
self.put('C/' + pad32(path.account) + '/' + key, DUMMY);
self.del('d/' + hash + '/' + pad32(i));
self.del('s/' + key);
self.del('o/' + key);
@ -967,7 +968,7 @@ TXDB.prototype._remove = function remove(tx, info, callback) {
for (i = 0; i < tx.outputs.length; i++) {
output = tx.outputs[i];
key = hash + '/' + i;
key = hash + '/' + pad32(i);
address = output.getHash('hex');
path = info.getPath(address);
@ -980,7 +981,7 @@ TXDB.prototype._remove = function remove(tx, info, callback) {
self.balance.sub(coin);
self.del('c/' + key);
self.del('C/' + path.account + '/' + key);
self.del('C/' + pad32(path.account) + '/' + key);
self.coinCache.remove(key);
}
@ -1065,13 +1066,13 @@ TXDB.prototype._unconfirm = function unconfirm(tx, info, callback, force) {
this.del('h/' + pad32(height) + '/' + hash);
for (i = 0; i < info.accounts.length; i++) {
id = info.accounts[i];
id = pad32(info.accounts[i]);
this.put('P/' + id + '/' + hash, DUMMY);
this.del('H/' + id + '/' + pad32(height) + '/' + hash);
}
utils.forEachSerial(tx.outputs, function(output, next, i) {
var key = hash + '/' + i;
var key = hash + '/' + pad32(i);
self.getCoin(hash, i, function(err, coin) {
if (err)
return next(err);
@ -1114,8 +1115,12 @@ TXDB.prototype.getHistoryHashes = function getHistoryHashes(account, callback) {
}
this.iterate({
gte: account != null ? 'T/' + account + '/' : 't',
lte: account != null ? 'T/' + account + '/~' : 't~',
gte: account != null
? 'T/' + pad32(account) + '/' + constants.NULL_HASH
: 't/' + constants.NULL_HASH,
lte: account != null
? 'T/' + pad32(account) + '/' + constants.HIGH_HASH
: 't/' + constants.HIGH_HASH,
transform: function(key) {
key = key.split('/');
if (account != null)
@ -1138,8 +1143,12 @@ TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(account, cal
}
this.iterate({
gte: account != null ? 'P/' + account + '/' : 'p',
lte: account != null ? 'P/' + account + '/~' : 'p~',
gte: account != null
? 'P/' + pad32(account) + '/' + constants.NULL_HASH
: 'p/' + constants.NULL_HASH,
lte: account != null
? 'P/' + pad32(account) + '/' + constants.HIGH_HASH
: 'p/' + constants.HIGH_HASH,
transform: function(key) {
key = key.split('/');
if (account != null)
@ -1162,8 +1171,12 @@ TXDB.prototype.getCoinHashes = function getCoinHashes(account, callback) {
}
this.iterate({
gte: account != null ? 'C/' + account + '/' : 'c',
lte: account != null ? 'C/' + account + '/~' : 'c~',
gte: account != null
? 'C/' + pad32(account) + '/' + constants.NULL_HASH + '/' + pad32(0)
: 'c/' + constants.NULL_HASH + '/' + pad32(0),
lte: account != null
? 'C/' + pad32(account) + '/' + constants.HIGH_HASH + '/' + pad32(0xffffffff)
: 'c/' + constants.HIGH_HASH + '/' + pad32(0xffffffff),
transform: function(key) {
key = key.split('/');
if (account != null)
@ -1193,11 +1206,11 @@ TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(account, opt
this.iterate({
gte: account != null
? 'H/' + account + '/' + pad32(options.start) + '/'
: 'h/' + pad32(options.start) + '/',
? 'H/' + pad32(account) + '/' + pad32(options.start) + '/' + constants.NULL_HASH
: 'h/' + pad32(options.start) + '/' + constants.NULL_HASH,
lte: account != null
? 'H/' + account + '/' + pad32(options.end) + '/~'
: 'h/' + pad32(options.end) + '/~',
? 'H/' + pad32(account) + '/' + pad32(options.end) + '/' + constants.HIGH_HASH
: 'h/' + pad32(options.end) + '/' + constants.HIGH_HASH,
limit: options.limit,
reverse: options.reverse,
transform: function(key) {
@ -1238,11 +1251,11 @@ TXDB.prototype.getRangeHashes = function getRangeHashes(account, options, callba
this.iterate({
gte: account != null
? 'M/' + account + '/' + pad32(options.start) + '/'
: 'm/' + pad32(options.start) + '/',
? 'M/' + pad32(account) + '/' + pad32(options.start) + '/' + constants.NULL_HASH
: 'm/' + pad32(options.start) + '/' + constants.NULL_HASH,
lte: account != null
? 'M/' + account + '/' + pad32(options.end) + '/~'
: 'm/' + pad32(options.end) + '/~',
? 'M/' + pad32(account) + '/' + pad32(options.end) + '/' + constants.HIGH_HASH
: 'm/' + pad32(options.end) + '/' + constants.HIGH_HASH,
limit: options.limit,
reverse: options.reverse,
transform: function(key) {
@ -1339,8 +1352,8 @@ TXDB.prototype.getHistory = function getHistory(account, callback) {
// Fast case
this.iterate({
gte: 't',
lte: 't~',
gte: 't/' + constants.NULL_HASH,
lte: 't/' + constants.HIGH_HASH,
values: true,
parse: function(data) {
return bcoin.tx.fromExtended(data);
@ -1483,8 +1496,8 @@ TXDB.prototype.getCoins = function getCoins(account, callback) {
// Fast case
this.iterate({
gte: 'c',
lte: 'c~',
gte: 'c/' + constants.NULL_HASH + '/' + pad32(0),
lte: 'c/' + constants.HIGH_HASH + '/' + pad32(0xffffffff),
keys: true,
values: true,
parse: function(data, key) {
@ -1494,7 +1507,7 @@ TXDB.prototype.getCoins = function getCoins(account, callback) {
var coin = bcoin.coin.fromRaw(data);
coin.hash = hash;
coin.index = index;
key = hash + '/' + index;
key = hash + '/' + pad32(index);
self.coinCache.set(key, data);
return coin;
}
@ -1553,8 +1566,8 @@ TXDB.prototype.fillHistory = function fillHistory(tx, callback) {
hash = tx.hash('hex');
this.iterate({
gte: 'd/' + hash + '/',
lte: 'd/' + hash + '/~',
gte: 'd/' + hash + '/' + pad32(0),
lte: 'd/' + hash + '/' + pad32(0xffffffff),
keys: true,
values: true,
parse: function(value, key) {
@ -1704,7 +1717,7 @@ TXDB.prototype.hasTX = function hasTX(hash, callback) {
TXDB.prototype.getCoin = function getCoin(hash, index, callback) {
var self = this;
var key = hash + '/' + index;
var key = hash + '/' + pad32(index);
var coin = this.coinCache.get(key);
if (coin) {
@ -1734,7 +1747,7 @@ TXDB.prototype.getCoin = function getCoin(hash, index, callback) {
*/
TXDB.prototype.hasCoin = function hasCoin(hash, index, callback) {
var key = hash + '/' + index;
var key = hash + '/' + pad32(index);
if (this.coinCache.has(key))
return callback(null, true);
@ -1766,11 +1779,11 @@ TXDB.prototype.getBalance = function getBalance(account, callback) {
return callback(null, this.balance);
// Fast case
balance = new Balance(this.wallet.id);
balance = new Balance(this.wallet);
this.iterate({
gte: 'c',
lte: 'c~',
gte: 'c/' + constants.NULL_HASH + '/' + pad32(0),
lte: 'c/' + constants.HIGH_HASH + '/' + pad32(0xffffffff),
keys: true,
values: true,
parse: function(data, key) {
@ -1789,7 +1802,7 @@ TXDB.prototype.getBalance = function getBalance(account, callback) {
else
balance.confirmed += value;
key = hash + '/' + index;
key = hash + '/' + pad32(index);
self.coinCache.set(key, data);
}
@ -1809,7 +1822,7 @@ TXDB.prototype.getBalance = function getBalance(account, callback) {
TXDB.prototype.getAccountBalance = function getBalance(account, callback) {
var self = this;
var balance = new Balance(this.wallet.id);
var balance = new Balance(this.wallet);
var key, coin;
function parse(data) {
@ -1831,7 +1844,7 @@ TXDB.prototype.getAccountBalance = function getBalance(account, callback) {
return callback(err);
utils.forEachSerial(hashes, function(hash, next) {
key = hash[0] + '/' + hash[1];
key = hash[0] + '/' + pad32(hash[1]);
coin = self.coinCache.get(key);
if (coin) {
@ -1934,22 +1947,23 @@ TXDB.prototype.abandon = function abandon(hash, callback, force) {
* Details
*/
function Details(db, id, tx, table) {
this.db = db;
this.network = db.network;
this.id = id;
this.hash = tx.hash('hex');
this.height = tx.height;
this.block = tx.block;
this.index = tx.index;
this.confirmations = tx.getConfirmations(this.db.height);
this.fee = tx.hasCoins() ? tx.getFee() : 0;
this.ts = tx.ts;
this.ps = tx.ps;
this.tx = tx;
function Details(info) {
this.db = info.db;
this.network = info.db.network;
this.id = info.id;
this.label = info.label;
this.hash = info.tx.hash('hex');
this.height = info.tx.height;
this.block = info.tx.block;
this.index = info.tx.index;
this.confirmations = info.tx.getConfirmations(this.db.height);
this.fee = info.tx.hasCoins() ? info.tx.getFee() : 0;
this.ts = info.tx.ts;
this.ps = info.tx.ps;
this.tx = info.tx;
this.inputs = [];
this.outputs = [];
this.init(table);
this.init(info.table);
}
Details.prototype.init = function init(table) {
@ -1980,6 +1994,7 @@ Details.prototype._insert = function _insert(vector, target, table) {
for (j = 0; j < paths.length; j++) {
path = paths[j];
if (path.id === this.id) {
path.label = this.label;
member.path = path;
break;
}
@ -1994,6 +2009,7 @@ Details.prototype.toJSON = function toJSON() {
var self = this;
return {
id: this.id,
label: this.label,
hash: utils.revHex(this.hash),
height: this.height,
block: this.block ? utils.revHex(this.block) : null,
@ -2038,8 +2054,9 @@ DetailsMember.prototype.toJSON = function toJSON(network) {
* Balance
*/
function Balance(id) {
this.id = id;
function Balance(wallet) {
this.id = wallet.id;
this.label = wallet.label;
this.unconfirmed = 0;
this.confirmed = 0;
this.total = 0;
@ -2074,6 +2091,7 @@ Balance.prototype.unconfirm = function unconfirm(value) {
Balance.prototype.toJSON = function toJSON() {
return {
id: this.id,
label: this.label,
unconfirmed: utils.btc(this.unconfirmed),
confirmed: utils.btc(this.confirmed),
total: utils.btc(this.total)

View File

@ -56,7 +56,8 @@ function Wallet(db, options) {
this.writeLock = new bcoin.locker(this);
this.fundLock = new bcoin.locker(this);
this.id = null;
this.id = 0;
this.label = null;
this.master = null;
this.initialized = false;
this.accountDepth = 0;
@ -80,7 +81,7 @@ utils.inherits(Wallet, EventEmitter);
Wallet.prototype.fromOptions = function fromOptions(options) {
var master = options.master;
var id, token;
var label, token;
if (!master)
master = bcoin.hd.fromMnemonic(null, this.network);
@ -105,13 +106,18 @@ Wallet.prototype.fromOptions = function fromOptions(options) {
this.accountDepth = options.accountDepth;
}
if (options.id) {
assert(utils.isAlpha(options.id), 'Wallet ID must be alphanumeric.');
id = options.id;
if (options.id != null) {
assert(utils.isNumber(options.id));
this.id = options.id;
}
if (!id)
id = this.getID();
if (options.label) {
assert(utils.isAlpha(options.label), 'Wallet ID must be alphanumeric.');
label = options.label;
}
if (!label)
label = this.getLabel();
if (options.token) {
assert(Buffer.isBuffer(options.token));
@ -127,7 +133,7 @@ Wallet.prototype.fromOptions = function fromOptions(options) {
if (!token)
token = this.getToken(this.master.key, this.tokenDepth);
this.id = id;
this.label = label;
this.token = token;
return this;
@ -420,7 +426,7 @@ Wallet.prototype.unlock = function unlock(passphrase, timeout, callback) {
* @returns {Base58String}
*/
Wallet.prototype.getID = function getID() {
Wallet.prototype.getLabel = function getLabel() {
var key, p, hash;
assert(this.master.key, 'Cannot derive id.');
@ -492,6 +498,7 @@ Wallet.prototype.createAccount = function createAccount(options, callback, force
options = {
network: self.network,
id: self.id,
label: self.label,
name: self.accountDepth === 0 ? 'default' : options.name,
witness: options.witness,
accountKey: key.hdPublicKey,
@ -570,12 +577,25 @@ Wallet.prototype.getAddresses = function getAddresses(callback) {
*/
Wallet.prototype.getAccount = function getAccount(account, callback) {
var self = this;
if (this.account) {
if (account === 0 || account === 'default')
return callback(null, this.account);
}
return this.db.getAccount(this.id, account, callback);
this.db.getAccount(this.id, account, function(err, account) {
if (err)
return callback(err);
if (!account)
return callback();
account.id = self.id;
account.label = self.label;
return callback(null, account);
});
};
/**
@ -1877,6 +1897,7 @@ Wallet.prototype.__defineGetter__('changeAddress', function() {
Wallet.prototype.inspect = function inspect() {
return {
id: this.id,
label: this.label,
network: this.network.type,
initialized: this.initialized,
accountDepth: this.accountDepth,
@ -1898,6 +1919,7 @@ Wallet.prototype.toJSON = function toJSON() {
return {
network: this.network.type,
id: this.id,
label: this.label,
initialized: this.initialized,
accountDepth: this.accountDepth,
token: this.token.toString('hex'),
@ -1914,8 +1936,9 @@ Wallet.prototype.toJSON = function toJSON() {
*/
Wallet.prototype.fromJSON = function fromJSON(json) {
assert(utils.isAlpha(json.id), 'Wallet ID must be alphanumeric.');
assert(utils.isNumber(json.id));
assert(typeof json.initialized === 'boolean');
assert(utils.isAlpha(json.label), 'Wallet ID must be alphanumeric.');
assert(utils.isNumber(json.accountDepth));
assert(typeof json.token === 'string');
assert(json.token.length === 64);
@ -1923,6 +1946,7 @@ Wallet.prototype.fromJSON = function fromJSON(json) {
this.network = bcoin.network.get(json.network);
this.id = json.id;
this.label = json.label;
this.initialized = json.initialized;
this.accountDepth = json.accountDepth;
this.token = new Buffer(json.token, 'hex');
@ -1940,7 +1964,8 @@ Wallet.prototype.toRaw = function toRaw(writer) {
var p = new BufferWriter(writer);
p.writeU32(this.network.magic);
p.writeVarString(this.id, 'utf8');
p.writeU32(this.id);
p.writeVarString(this.label, 'utf8');
p.writeU8(this.initialized ? 1 : 0);
p.writeU32(this.accountDepth);
p.writeBytes(this.token);
@ -1962,7 +1987,8 @@ Wallet.prototype.toRaw = function toRaw(writer) {
Wallet.prototype.fromRaw = function fromRaw(data) {
var p = new BufferReader(data);
this.network = bcoin.network.fromMagic(p.readU32());
this.id = p.readVarString('utf8');
this.id = p.readU32();
this.label = p.readVarString('utf8');
this.initialized = p.readU8() === 1;
this.accountDepth = p.readU32();
this.token = p.readBytes(32);
@ -2043,7 +2069,8 @@ function Account(db, options) {
this.receiveAddress = null;
this.changeAddress = null;
this.id = null;
this.id = 0;
this.label = null;
this.name = null;
this.witness = this.db.options.witness;
this.accountKey = null;
@ -2072,11 +2099,13 @@ Account.prototype.fromOptions = function fromOptions(options) {
var i;
assert(options, 'Options are required.');
assert(utils.isAlpha(options.id), 'Wallet ID must be alphanumeric.');
assert(utils.isNumber(options.id));
assert(utils.isAlpha(options.label), 'Wallet ID must be alphanumeric.');
assert(bcoin.hd.isHD(options.accountKey), 'Account key is required.');
assert(utils.isNumber(options.accountIndex), 'Account index is required.');
this.id = options.id;
this.label = options.label;
if (options.name != null) {
assert(utils.isAlpha(options.name), 'Account name must be alphanumeric.');
@ -2481,6 +2510,7 @@ Account.prototype.deriveAddress = function deriveAddress(change, index) {
network: this.network,
key: key.publicKey,
id: this.id,
label: this.label,
name: this.name,
account: this.accountIndex,
change: change,
@ -2684,7 +2714,9 @@ Account.prototype.toRaw = function toRaw(writer) {
var i;
p.writeU32(this.network.magic);
p.writeVarString(this.id, 'utf8');
// NOTE: Passed in by caller.
// p.writeU32(this.id);
// p.writeVarString(this.label, 'utf8');
p.writeVarString(this.name, 'utf8');
p.writeU8(this.initialized ? 1 : 0);
p.writeU8(this.type === 'pubkeyhash' ? 0 : 1);
@ -2718,7 +2750,9 @@ Account.prototype.fromRaw = function fromRaw(data) {
var i, count;
this.network = bcoin.network.fromMagic(p.readU32());
this.id = p.readVarString('utf8');
// NOTE: Passed in by caller.
// this.id = p.readU32();
// this.label = p.readVarString('utf8');
this.name = p.readVarString('utf8');
this.initialized = p.readU8() === 1;
this.type = p.readU8() === 0 ? 'pubkeyhash' : 'multisig';

View File

@ -10,18 +10,20 @@
/*
* Database Layout:
* (inherits all from txdb)
* W/[address] -> id & path data
* p/[address] -> id & path data
* w/[id] -> wallet
* l/[label] -> wallet id
* a/[id]/[index] -> account
* i/[id]/[name] -> account index
* R -> tip
* b/[hash] -> wallet block
* e/[hash] -> tx->wallet-id map
* t/[hash] -> tx->wallet-id map
*/
var bcoin = require('./env');
var AsyncObject = require('./async');
var utils = require('./utils');
var pad32 = utils.pad32;
var assert = utils.assert;
var constants = bcoin.protocol.constants;
var BufferReader = require('./reader');
@ -60,11 +62,13 @@ function WalletDB(options) {
this.tip = this.network.genesis.hash;
this.height = 0;
this.depth = 0;
// We need one read lock for `get` and `create`.
// It will hold locks specific to wallet ids.
this.readLock = new ReadLock(this);
this.locker = new bcoin.locker(this);
this.readLock = new MappedLock(this);
this.writeLock = new MappedLock(this);
this.txLock = new bcoin.locker(this);
this.walletCache = new bcoin.lru(10000, 1);
this.accountCache = new bcoin.lru(10000, 1);
@ -131,7 +135,13 @@ WalletDB.prototype._open = function open(callback) {
if (err)
return callback(err);
self.loadFilter(callback);
self.getDepth(function(err, depth) {
if (err)
return callback(err);
self.depth = depth;
self.loadFilter(callback);
});
});
});
});
@ -160,12 +170,54 @@ WalletDB.prototype._close = function close(callback) {
};
/**
* Invoke mutex lock.
* @returns {Function} unlock
* Get current wallet ID depth.
* @private
* @param {Function} callback
*/
WalletDB.prototype._lock = function lock(id, func, args, force) {
return this.readLock.lock(id, func, args, force);
WalletDB.prototype.getDepth = function getDepth(callback) {
var opt, iter, parts, depth;
// This may seem like a strange way to do
// this, but updating a global state when
// creating a new wallet is actually pretty
// damn tricky. They would be major atomicity
// issues if updating a global state inside
// a "scoped" state. So, we avoid all the
// nonsense of adding a global lock to
// walletdb.create by simply seeking to the
// highest wallet id.
iter = this.db.iterator({
gte: 'w/' + pad32(0),
lte: 'w/' + pad32(0xffffffff),
reverse: true,
keys: true,
values: false,
fillCache: false,
keyAsBuffer: false,
valueAsBuffer: true
});
iter.next(function(err, key, value) {
if (err) {
return iter.end(function() {
callback(err);
});
}
iter.end(function(err) {
if (err)
return callback(err);
if (key === undefined)
return callback(null, 1);
parts = key.split('/');
depth = +parts[1];
callback(null, depth + 1);
});
});
};
/**
@ -175,7 +227,7 @@ WalletDB.prototype._lock = function lock(id, func, args, force) {
*/
WalletDB.prototype.start = function start(id) {
assert(utils.isAlpha(id), 'Bad ID for batch.');
assert(utils.isNumber(id), 'Bad ID for batch.');
assert(!this.batches[id], 'Batch already started.');
this.batches[id] = this.db.batch();
};
@ -201,7 +253,7 @@ WalletDB.prototype.drop = function drop(id) {
WalletDB.prototype.batch = function batch(id) {
var batch;
assert(utils.isAlpha(id), 'Bad ID for batch.');
assert(utils.isNumber(id), 'Bad ID for batch.');
batch = this.batches[id];
assert(batch, 'Batch does not exist.');
return batch;
@ -271,8 +323,8 @@ WalletDB.prototype.testFilter = function testFilter(addresses) {
WalletDB.prototype.dump = function dump(callback) {
var records = {};
this.db.each({
gte: 'w',
lte: 'w~',
gte: ' ',
lte: '~',
keys: true,
values: true
}, function(key, value, next) {
@ -306,6 +358,33 @@ WalletDB.prototype.unregister = function unregister(wallet) {
delete this.wallets[wallet.id];
};
/**
* Map wallet label to wallet id.
* @param {String} label
* @param {Function} callback
*/
WalletDB.prototype.getWalletID = function getWalletID(label, callback) {
var id;
if (!label)
return callback();
if (typeof label === 'number')
return callback(null, label);
id = this.walletCache.get(label);
if (id)
return callback(null, id);
this.db.fetch('l/' + label, function(data) {
id = data.readUInt32LE(0, true);
self.walletCache.set(label, id);
return id;
}, callback);
};
/**
* Get a wallet from the database, setup watcher.
* @param {WalletID} id
@ -314,41 +393,36 @@ WalletDB.prototype.unregister = function unregister(wallet) {
WalletDB.prototype.get = function get(id, callback) {
var self = this;
var unlock, wallet;
unlock = this._lock(id, get, [id, callback]);
if (!unlock)
return;
callback = utils.wrap(callback, unlock);
if (!id)
return callback();
wallet = this.wallets[id];
if (wallet)
return callback(null, wallet);
this._get(id, function(err, wallet) {
this.getWalletID(id, function(err, id) {
if (err)
return callback(err);
if (!wallet)
if (!id)
return callback();
try {
self.register(wallet);
} catch (e) {
return callback(e);
}
wallet.open(function(err) {
self._get(id, function(err, wallet, watched) {
if (err)
return callback(err);
return callback(null, wallet);
if (!wallet)
return callback();
if (watched)
return callback(null, wallet);
try {
self.register(wallet);
} catch (e) {
return callback(e);
}
wallet.open(function(err) {
if (err)
return callback(err);
return callback(null, wallet);
});
});
});
};
@ -362,33 +436,23 @@ WalletDB.prototype.get = function get(id, callback) {
WalletDB.prototype._get = function get(id, callback) {
var self = this;
var wallet;
var unlock, wallet;
if (!id)
return callback();
unlock = this.readLock.lock(id, get, [id, callback]);
wallet = this.walletCache.get(id);
if (!unlock)
return;
callback = utils.wrap(callback, unlock);
wallet = this.wallets[id];
if (wallet)
return callback(null, wallet);
return callback(null, wallet, true);
this.db.get('w/' + id, function(err, data) {
if (err)
return callback(err);
if (!data)
return callback();
try {
wallet = bcoin.wallet.fromRaw(self, data);
} catch (e) {
return callback(e);
}
self.walletCache.set(id, wallet);
return callback(null, wallet);
});
this.db.fetch('w/' + pad32(id), function(data) {
return bcoin.wallet.fromRaw(self, data);
}, callback);
};
/**
@ -399,8 +463,11 @@ WalletDB.prototype._get = function get(id, callback) {
WalletDB.prototype.save = function save(wallet) {
var batch = this.batch(wallet.id);
this.walletCache.set(wallet.id, wallet);
batch.put('w/' + wallet.id, wallet.toRaw());
var id = new Buffer(4);
this.walletCache.set(wallet.label, wallet.id);
batch.put('w/' + pad32(wallet.id), wallet.toRaw());
id.writeUInt32LE(wallet.id, 0, true);
batch.put('l/' + wallet.label, id);
};
/**
@ -447,14 +514,14 @@ WalletDB.prototype.create = function create(options, callback) {
options = {};
}
unlock = this._lock(options.id, create, [options, callback]);
unlock = this.writeLock.lock(options.label, create, [options, callback]);
if (!unlock)
return;
callback = utils.wrap(callback, unlock);
this.has(options.id, function(err, exists) {
this.has(options.label, function(err, exists) {
if (err)
return callback(err);
@ -466,6 +533,7 @@ WalletDB.prototype.create = function create(options, callback) {
try {
wallet = bcoin.wallet.fromOptions(self, options);
wallet.id = self.depth++;
} catch (e) {
return callback(e);
}
@ -480,7 +548,7 @@ WalletDB.prototype.create = function create(options, callback) {
if (err)
return callback(err);
self.logger.info('Created wallet %s.', wallet.id);
self.logger.info('Created wallet %s.', wallet.label);
return callback(null, wallet);
});
@ -494,16 +562,11 @@ WalletDB.prototype.create = function create(options, callback) {
*/
WalletDB.prototype.has = function has(id, callback) {
if (!id)
return callback(null, false);
if (this.wallets[id])
return callback(null, true);
if (this.walletCache.has(id))
return callback(null, true);
this.db.has('w/' + id, callback);
this.getWalletID(id, function(err, id) {
if (err)
return callback(err);
return callback(null, id != null);
});
};
/**
@ -516,7 +579,7 @@ WalletDB.prototype.has = function has(id, callback) {
WalletDB.prototype.ensure = function ensure(options, callback) {
var self = this;
this.get(options.id, function(err, wallet) {
this.get(options.label, function(err, wallet) {
if (err)
return callback(err);
@ -571,29 +634,17 @@ WalletDB.prototype.getAccount = function getAccount(id, name, callback) {
WalletDB.prototype._getAccount = function getAccount(id, index, callback) {
var self = this;
var key = id + '/' + index;
var key = pad32(id) + '/' + pad32(index);
var account = this.accountCache.get(key);
if (account)
return callback(null, account);
this.db.get('a/' + key, function(err, data) {
if (err)
return callback(err);
if (!data)
return callback();
try {
account = bcoin.account.fromRaw(self, data);
} catch (e) {
return callback(e);
}
this.db.fetch('a/' + key, function(data) {
account = bcoin.account.fromRaw(self, data);
self.accountCache.set(key, account);
return callback(null, account);
});
return account;
}, callback);
};
/**
@ -606,12 +657,12 @@ WalletDB.prototype.getAccounts = function getAccounts(id, callback) {
var map = [];
var i, accounts;
if (!utils.isAlpha(id))
if (!utils.isNumber(id))
return callback(new Error('Wallet IDs must be alphanumeric.'));
this.db.iterate({
gte: 'i/' + id + '/',
lte: 'i/' + id + '/~',
gte: 'i/' + pad32(id) + '/',
lte: 'i/' + pad32(id) + '/~',
values: true,
parse: function(value, key) {
var name = key.split('/')[2];
@ -651,7 +702,7 @@ WalletDB.prototype.getAccountIndex = function getAccountIndex(id, name, callback
if (typeof name === 'number')
return callback(null, name);
this.db.get('i/' + id + '/' + name, function(err, index) {
this.db.get('i/' + pad32(id) + '/' + name, function(err, index) {
if (err)
return callback(err);
@ -671,12 +722,12 @@ WalletDB.prototype.getAccountIndex = function getAccountIndex(id, name, callback
WalletDB.prototype.saveAccount = function saveAccount(account) {
var batch = this.batch(account.id);
var index = new Buffer(4);
var key = account.id + '/' + account.accountIndex;
var key = pad32(account.id) + '/' + pad32(account.accountIndex);
index.writeUInt32LE(account.accountIndex, 0, true);
batch.put('a/' + key, account.toRaw());
batch.put('i/' + account.id + '/' + account.name, index);
batch.put('i/' + pad32(account.id) + '/' + account.name, index);
this.accountCache.set(key, account);
};
@ -742,7 +793,7 @@ WalletDB.prototype.hasAccount = function hasAccount(id, account, callback) {
if (index === -1)
return callback(null, false);
key = id + '/' + index;
key = pad32(id) + '/' + pad32(index);
if (self.accountCache.has(key))
return callback(null, true);
@ -754,7 +805,7 @@ WalletDB.prototype.hasAccount = function hasAccount(id, account, callback) {
/**
* Save an address to the path map.
* The path map exists in the form of:
* `W/[address-hash] -> {walletid1=path1, walletid2=path2, ...}`
* `p/[address-hash] -> {walletid1=path1, walletid2=path2, ...}`
* @param {WalletID} id
* @param {KeyRing[]} addresses
* @param {Function} callback
@ -806,7 +857,7 @@ WalletDB.prototype.saveAddress = function saveAddress(id, addresses, callback) {
self.pathCache.set(hash, paths);
batch.put('W/' + hash, serializePaths(paths));
batch.put('p/' + hash, serializePaths(paths));
next();
});
@ -831,7 +882,7 @@ WalletDB.prototype._getPaths = function _getPaths(hash, callback) {
if (paths)
return callback(null, paths);
this.db.fetch('W/' + hash, parsePaths, function(err, paths) {
this.db.fetch('p/' + hash, parsePaths, function(err, paths) {
if (err)
return callback(err);
@ -995,7 +1046,7 @@ WalletDB.prototype.mapWallets = function mapWallets(tx, callback) {
* @param {Function} callback - Returns [Error, {@link PathInfo}].
*/
WalletDB.prototype.getPathInfo = function getPathInfo(id, tx, callback) {
WalletDB.prototype.getPathInfo = function getPathInfo(wallet, tx, callback) {
var self = this;
var addresses = tx.getHashes('hex');
var info;
@ -1007,7 +1058,8 @@ WalletDB.prototype.getPathInfo = function getPathInfo(id, tx, callback) {
if (!table)
return callback();
info = new PathInfo(self, id, tx, table);
info = new PathInfo(self, wallet.id, tx, table);
info.label = wallet.label;
return callback(null, info);
});
@ -1131,7 +1183,7 @@ WalletDB.prototype.writeBlock = function writeBlock(block, matches, callback) {
for (i = 0; i < block.hashes.length; i++) {
hash = block.hashes[i];
wallets = matches[i];
batch.put('e/' + hash, serializeWallets(wallets));
batch.put('t/' + hash, serializeWallets(wallets));
}
}
@ -1189,7 +1241,7 @@ WalletDB.prototype.getBlock = function getBlock(hash, callback) {
*/
WalletDB.prototype.getWalletsByTX = function getWalletsByTX(hash, callback) {
this.db.fetch('e/' + hash, parseWallets, callback);
this.db.fetch('t/' + hash, parseWallets, callback);
};
/**
@ -1202,7 +1254,7 @@ WalletDB.prototype.addBlock = function addBlock(entry, txs, callback, force) {
var self = this;
var i, block, matches, hash, unlock;
unlock = this.locker.lock(addBlock, [entry, txs, callback], force);
unlock = this.txLock.lock(addBlock, [entry, txs, callback], force);
if (!unlock)
return;
@ -1253,7 +1305,7 @@ WalletDB.prototype.removeBlock = function removeBlock(entry, callback, force) {
var self = this;
var unlock;
unlock = this.locker.lock(removeBlock, [entry, callback], force);
unlock = this.txLock.lock(removeBlock, [entry, callback], force);
if (!unlock)
return;
@ -1342,6 +1394,8 @@ WalletDB.prototype.addTX = function addTX(tx, callback, force) {
self.logger.debug('Adding tx to wallet: %s', info.id);
info.label = wallet.label;
wallet.tx.add(tx, info, function(err) {
if (err)
return next(err);
@ -1397,7 +1451,9 @@ function Path() {
this.account = 0;
this.change = 0;
this.index = 0;
this.address = null;
// NOTE: Passed in by caller.
this.label = null;
}
/**
@ -1408,7 +1464,7 @@ function Path() {
Path.prototype.fromRaw = function fromRaw(data) {
var p = new BufferReader(data);
this.id = p.readVarString('utf8');
this.id = p.readU32();
this.name = p.readVarString('utf8');
this.account = p.readU32();
this.change = p.readU32();
@ -1434,7 +1490,7 @@ Path.fromRaw = function fromRaw(data) {
Path.prototype.toRaw = function toRaw(writer) {
var p = new BufferWriter(writer);
p.writeVarString(this.id, 'utf8');
p.writeU32(this.id);
p.writeVarString(this.name, 'utf8');
p.writeU32(this.account);
p.writeU32(this.change);
@ -1455,6 +1511,7 @@ Path.prototype.toRaw = function toRaw(writer) {
Path.prototype.fromKeyRing = function fromKeyRing(address) {
this.id = address.id;
this.label = address.label;
this.name = address.name;
this.account = address.account;
this.change = address.change;
@ -1492,6 +1549,7 @@ Path.prototype.toPath = function() {
Path.prototype.toJSON = function toJSON() {
return {
id: this.id,
label: this.label,
name: this.name,
change: this.change === 1,
path: this.toPath()
@ -1512,6 +1570,7 @@ Path.prototype.fromJSON = function fromJSON(json) {
indexes[0] -= constants.hd.HARDENED;
this.id = json.id;
this.label = json.label;
this.name = json.name;
this.account = indexes[0];
this.change = indexes[1];
@ -1530,22 +1589,14 @@ Path.fromJSON = function fromJSON(json) {
return new Path().fromJSON(json);
};
/**
* Convert path to a key in the form of (id|account).
* @returns {String}
*/
Path.prototype.toKey = function toKey() {
return this.id + '/' + this.account;
};
/**
* Inspect the path.
* @returns {String}
*/
Path.prototype.inspect = function() {
return '<Path: ' + this.id
return '<Path: ' + this.label
+ '(' + this.id + ')'
+ '/' + this.name
+ ': ' + this.toPath()
+ '>';
@ -1572,6 +1623,9 @@ function PathInfo(db, id, tx, table) {
// Wallet ID
this.id = id;
// Wallet Label (passed in by caller).
this.label = null;
// Map of address hashes->paths (for everything).
this.table = null;
@ -1610,7 +1664,7 @@ PathInfo.map = function map(db, tx, table) {
return;
for (i = 0; i < wallets.length; i++) {
id = wallets[i];
id = +wallets[i];
info.push(new PathInfo(db, id, tx, table));
}
@ -1692,7 +1746,7 @@ PathInfo.prototype.toDetails = function toDetails() {
var details = this._details;
if (!details) {
details = new TXDB.Details(this.db, this.id, this.tx, this.table);
details = new TXDB.Details(this);
this._details = details;
}
@ -1747,7 +1801,7 @@ function serializeWallets(wallets) {
for (i = 0; i < wallets.length; i++) {
info = wallets[i];
p.writeVarBytes(info.id, 'ascii');
p.writeU32(info.id);
}
return p.render();
@ -1757,7 +1811,7 @@ function parseWallets(data) {
var p = new BufferReader(data);
var wallets = [];
while (p.left())
wallets.push(p.readVarString('ascii'));
wallets.push(p.readU32());
return wallets;
}
@ -1843,33 +1897,33 @@ WalletBlock.prototype.toJSON = function toJSON() {
};
};
function ReadLock(parent) {
if (!(this instanceof ReadLock))
return new ReadLock(parent);
function MappedLock(parent) {
if (!(this instanceof MappedLock))
return new MappedLock(parent);
this.parent = parent;
this.jobs = [];
this.busy = {};
}
ReadLock.prototype.lock = function lock(id, func, args, force) {
MappedLock.prototype.lock = function lock(key, func, args, force) {
var self = this;
var called;
if (force || !id) {
assert(!id || this.busy[id]);
if (force || key == null) {
assert(key == null || this.busy[key]);
return function unlock() {
assert(!called);
called = true;
};
}
if (this.busy[id]) {
if (this.busy[key]) {
this.jobs.push([func, args]);
return;
}
this.busy[id] = true;
this.busy[key] = true;
return function unlock() {
var item;
@ -1877,7 +1931,7 @@ ReadLock.prototype.lock = function lock(id, func, args, force) {
assert(!called);
called = true;
delete self.busy[id];
delete self.busy[key];
if (self.jobs.length === 0)
return;

View File

@ -1001,7 +1001,11 @@ describe('Wallet', function() {
});
it('should cleanup', function(cb) {
constants.tx.COINBASE_MATURITY = 100;
cb();
walletdb.dump(function(err, records) {
assert.ifError(err);
// utils.log(JSON.stringify(Object.keys(records), null, 2));
constants.tx.COINBASE_MATURITY = 100;
cb();
});
});
});