wallet: more rewriting.

This commit is contained in:
Christopher Jeffrey 2016-10-01 12:31:43 -07:00
parent 2544e5310a
commit 61a77d90e9
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
11 changed files with 706 additions and 359 deletions

View File

@ -1238,7 +1238,7 @@ ChainDB.prototype.getCoinsByAddress = co(function* getCoinsByAddress(addresses)
if (!hash)
continue;
keys = yield this.db.iterate({
keys = yield this.db.keys({
gte: layout.C(hash, constants.ZERO_HASH, 0),
lte: layout.C(hash, constants.MAX_HASH, 0xffffffff),
parse: layout.Cc
@ -1263,11 +1263,9 @@ ChainDB.prototype.getCoinsByAddress = co(function* getCoinsByAddress(addresses)
ChainDB.prototype.getEntries = function getEntries() {
var self = this;
return this.db.iterate({
return this.db.values({
gte: layout.e(constants.ZERO_HASH),
lte: layout.e(constants.MAX_HASH),
keys: false,
values: true,
parse: function(key, value) {
return bcoin.chainentry.fromRaw(self.chain, value);
}
@ -1294,7 +1292,7 @@ ChainDB.prototype.getHashesByAddress = co(function* getHashesByAddress(addresses
if (!hash)
continue;
yield this.db.iterate({
yield this.db.keys({
gte: layout.T(hash, constants.ZERO_HASH),
lte: layout.T(hash, constants.MAX_HASH),
parse: function(key) {

View File

@ -301,14 +301,17 @@ LowlevelUp.prototype.has = co(function* has(key) {
* @returns {Promise} - Returns Array.
*/
LowlevelUp.prototype.iterate = co(function* iterate(options) {
LowlevelUp.prototype.range = co(function* range(options) {
var items = [];
var parse = options.parse;
var iter, item, data;
var iter, item;
assert(typeof parse === 'function', 'Parse must be a function.');
iter = this.iterator(options);
iter = this.iterator({
gte: options.gte,
lte: options.lte,
keys: true,
values: true
});
for (;;) {
item = yield iter.next();
@ -316,20 +319,130 @@ LowlevelUp.prototype.iterate = co(function* iterate(options) {
if (!item)
break;
try {
data = parse(item.key, item.value);
} catch (e) {
yield iter.end();
throw e;
if (parse) {
try {
item = parse(item.key, item.value);
} catch (e) {
yield iter.end();
throw e;
}
}
if (data)
items.push(data);
if (item)
items.push(item);
}
return items;
});
/**
* Collect all keys from iterator options.
* @param {Object} options - Iterator options.
* @returns {Promise} - Returns Array.
*/
LowlevelUp.prototype.keys = co(function* keys(options) {
var keys = [];
var parse = options.parse;
var iter, item, key;
iter = this.iterator({
gte: options.gte,
lte: options.lte,
keys: true,
values: false
});
for (;;) {
item = yield iter.next();
if (!item)
break;
key = item.key;
if (parse) {
try {
key = parse(key);
} catch (e) {
yield iter.end();
throw e;
}
}
if (key)
keys.push(key);
}
return keys;
});
/**
* Collect all keys from iterator options.
* @param {Object} options - Iterator options.
* @returns {Promise} - Returns Array.
*/
LowlevelUp.prototype.values = co(function* values(options) {
var values = [];
var parse = options.parse;
var iter, item, value;
iter = this.iterator({
gte: options.gte,
lte: options.lte,
keys: false,
values: true
});
for (;;) {
item = yield iter.next();
if (!item)
break;
value = item.value;
if (parse) {
try {
value = parse(value);
} catch (e) {
yield iter.end();
throw e;
}
}
if (value)
values.push(value);
}
return values;
});
/**
* Dump database (for debugging).
* @returns {Promise} - Returns Object.
*/
LowlevelUp.prototype.dump = co(function* dump() {
var records = {};
var i, items, item, key, value;
items = yield this.range({
gte: new Buffer([0x00]),
lte: new Buffer([0xff])
});
for (i = 0; i < items.length; i++) {
item = items[i];
key = item.key.toString('hex');
value = item.value.toString('hex');
records[key] = value;
}
return records;
});
/**
* Write and assert a version number for the database.
* @param {Number} version

View File

@ -16,6 +16,8 @@ var networks = bcoin.networks;
var BufferReader = require('../utils/reader');
var BufferWriter = require('../utils/writer');
var scriptTypes = constants.scriptTypes;
var Address = require('./address');
var ec = require('../crypto/ec');
/**
* Represents a key ring which amounts to an address.
@ -61,6 +63,9 @@ function KeyRing(options, network) {
KeyRing.prototype.fromOptions = function fromOptions(options, network) {
var key = toKey(options);
var script = options.script;
var compressed = options.compressed;
var network = options.network;
if (Buffer.isBuffer(key))
return this.fromKey(key, network);
@ -73,18 +78,15 @@ KeyRing.prototype.fromOptions = function fromOptions(options, network) {
if (options.privateKey)
key = toKey(options.privateKey);
if (options.network)
this.network = bcoin.network.get(options.network);
if (options.witness != null) {
assert(typeof options.witness === 'boolean');
this.witness = options.witness;
}
if (options.script)
return this.fromScript(key, options.script, this.network);
if (script)
return this.fromScript(key, script, compressed, network);
this.fromKey(key, this.network);
this.fromKey(key, compressed, network);
};
/**
@ -100,44 +102,51 @@ KeyRing.fromOptions = function fromOptions(options) {
/**
* Inject data from private key.
* @private
* @param {Buffer} privateKey
* @param {Buffer} key
* @param {Boolean?} compressed
* @param {(NetworkType|Network}) network
*/
KeyRing.prototype.fromPrivate = function fromPrivate(privateKey, network) {
assert(Buffer.isBuffer(privateKey), 'Private key must be a buffer.');
assert(bcoin.ec.privateKeyVerify(privateKey), 'Not a valid private key.');
KeyRing.prototype.fromPrivate = function fromPrivate(key, compressed, network) {
assert(Buffer.isBuffer(key), 'Private key must be a buffer.');
assert(ec.privateKeyVerify(key), 'Not a valid private key.');
if (typeof compressed !== 'boolean') {
network = compressed;
compressed = null;
}
this.network = bcoin.network.get(network);
this.privateKey = privateKey;
this.publicKey = bcoin.ec.publicKeyCreate(this.privateKey, true);
this.privateKey = key;
this.publicKey = ec.publicKeyCreate(key, compressed !== false);
return this;
};
/**
* Instantiate keyring from a private key.
* @param {Buffer} privateKey
* @param {Buffer} key
* @param {Boolean?} compressed
* @param {(NetworkType|Network}) network
* @returns {KeyRing}
*/
KeyRing.fromPrivate = function fromPrivate(privateKey, network) {
return new KeyRing().fromPrivate(privateKey, network);
KeyRing.fromPrivate = function fromPrivate(key, compressed, network) {
return new KeyRing().fromPrivate(key, compressed, network);
};
/**
* Inject data from public key.
* @private
* @param {Buffer} privateKey
* @param {Buffer} key
* @param {(NetworkType|Network}) network
*/
KeyRing.prototype.fromPublic = function fromPublic(publicKey, network) {
assert(Buffer.isBuffer(publicKey), 'Public key must be a buffer.');
assert(bcoin.ec.publicKeyVerify(publicKey), 'Not a valid public key.');
KeyRing.prototype.fromPublic = function fromPublic(key, network) {
assert(Buffer.isBuffer(key), 'Public key must be a buffer.');
assert(ec.publicKeyVerify(key), 'Not a valid public key.');
this.network = bcoin.network.get(network);
this.publicKey = publicKey;
this.publicKey = key;
return this;
};
@ -147,12 +156,17 @@ KeyRing.prototype.fromPublic = function fromPublic(publicKey, network) {
* @returns {KeyRing}
*/
KeyRing.generate = function(network) {
var key = new KeyRing();
key.network = bcoin.network.get(network);
key.privateKey = bcoin.ec.generatePrivateKey();
key.publicKey = bcoin.ec.publicKeyCreate(key.privateKey, true);
return key;
KeyRing.generate = function(compressed, network) {
var key;
if (typeof compressed !== 'boolean') {
network = compressed;
compressed = null;
}
key = ec.generatePrivateKey();
return KeyRing.fromKey(key, compressed, network);
};
/**
@ -162,8 +176,8 @@ KeyRing.generate = function(network) {
* @returns {KeyRing}
*/
KeyRing.fromPublic = function fromPublic(publicKey, network) {
return new KeyRing().fromPublic(publicKey, network);
KeyRing.fromPublic = function fromPublic(key, network) {
return new KeyRing().fromPublic(key, network);
};
/**
@ -173,14 +187,18 @@ KeyRing.fromPublic = function fromPublic(publicKey, network) {
* @param {(NetworkType|Network}) network
*/
KeyRing.prototype.fromKey = function fromKey(key, network) {
KeyRing.prototype.fromKey = function fromKey(key, compressed, network) {
assert(Buffer.isBuffer(key), 'Key must be a buffer.');
assert(key.length === 32 || key.length === 33, 'Not a key.');
if (key.length === 33)
return this.fromPublic(key, network);
if (typeof compressed !== 'boolean') {
network = compressed;
compressed = null;
}
return this.fromPrivate(key, network);
if (key.length === 32)
return this.fromPrivate(key, compressed !== false, network);
return this.fromPublic(key, network);
};
/**
@ -190,8 +208,8 @@ KeyRing.prototype.fromKey = function fromKey(key, network) {
* @returns {KeyRing}
*/
KeyRing.fromKey = function fromKey(key, network) {
return new KeyRing().fromKey(key, network);
KeyRing.fromKey = function fromKey(key, compressed, network) {
return new KeyRing().fromKey(key, compressed, network);
};
/**
@ -202,10 +220,17 @@ KeyRing.fromKey = function fromKey(key, network) {
* @param {(NetworkType|Network}) network
*/
KeyRing.prototype.fromScript = function fromScript(key, script, network) {
KeyRing.prototype.fromScript = function fromScript(key, script, compressed, network) {
assert(script instanceof bcoin.script, 'Non-script passed into KeyRing.');
this.fromKey(key, network);
if (typeof compressed !== 'boolean') {
network = compressed;
compressed = null;
}
this.fromKey(key, compressed, network);
this.script = script;
return this;
};
@ -217,8 +242,8 @@ KeyRing.prototype.fromScript = function fromScript(key, script, network) {
* @returns {KeyRing}
*/
KeyRing.fromScript = function fromScript(key, script, network) {
return new KeyRing().fromScript(key, script, network);
KeyRing.fromScript = function fromScript(key, script, compressed, network) {
return new KeyRing().fromScript(key, script, compressed, network);
};
/**
@ -235,7 +260,8 @@ KeyRing.prototype.toSecret = function toSecret() {
p.writeU8(this.network.keyPrefix.privkey);
p.writeBytes(this.privateKey);
p.writeU8(1);
if (this.publicKey.length === 33)
p.writeU8(1);
p.writeChecksum();
@ -274,9 +300,7 @@ KeyRing.prototype.fromSecret = function fromSecret(data) {
p.verifyChecksum();
assert(compressed === false, 'Cannot handle uncompressed.');
return this.fromPrivate(key, type);
return this.fromPrivate(key, compressed, type);
};
/**
@ -528,7 +552,7 @@ KeyRing.prototype.getKeyAddress = function getKeyAddress(enc) {
*/
KeyRing.prototype.compile = function compile(hash, type, version) {
return bcoin.address.fromHash(hash, type, version, this.network);
return Address.fromHash(hash, type, version, this.network);
};
/**
@ -651,7 +675,7 @@ KeyRing.prototype.getRedeem = function(hash) {
KeyRing.prototype.sign = function sign(msg) {
assert(this.privateKey, 'Cannot sign without private key.');
return bcoin.ec.sign(msg, this.privateKey);
return ec.sign(msg, this.privateKey);
};
/**
@ -662,7 +686,7 @@ KeyRing.prototype.sign = function sign(msg) {
*/
KeyRing.prototype.verify = function verify(msg, sig) {
return bcoin.ec.verify(msg, sig, this.publicKey);
return ec.verify(msg, sig, this.publicKey);
};
/**
@ -680,6 +704,22 @@ KeyRing.prototype.getType = function getType() {
return scriptTypes.PUBKEYHASH;
};
/**
* Get address type.
* @returns {ScriptType}
*/
KeyRing.prototype.getAddressType = function getAddressType() {
if (this.witness) {
if (this.script)
return scriptTypes.WITNESSSCRIPTHASH;
return scriptTypes.WITNESSPUBKEYHASH;
}
if (this.script)
return scriptTypes.SCRIPTHASH;
return scriptTypes.PUBKEYHASH;
};
/*
* Getters
*/
@ -807,10 +847,12 @@ KeyRing.prototype.toRaw = function toRaw(writer) {
p.writeU8(this.witness ? 1 : 0);
if (this.privateKey)
if (this.privateKey) {
p.writeVarBytes(this.privateKey);
else
p.writeU8(this.publicKey.length === 33);
} else {
p.writeVarBytes(this.publicKey);
}
if (this.script)
p.writeVarBytes(this.script.toRaw());
@ -831,7 +873,7 @@ KeyRing.prototype.toRaw = function toRaw(writer) {
KeyRing.prototype.fromRaw = function fromRaw(data, network) {
var p = new BufferReader(data);
var key, script;
var compressed, key, script;
this.network = bcoin.network.get(network);
this.witness = p.readU8() === 1;
@ -839,10 +881,12 @@ KeyRing.prototype.fromRaw = function fromRaw(data, network) {
key = p.readVarBytes();
if (key.length === 32) {
compressed = p.readU8() === 1;
this.privateKey = key;
this.publicKey = bcoin.ec.publicKeyCreate(key, true);
this.publicKey = ec.publicKeyCreate(key, compressed);
} else {
this.publicKey = key;
assert(ec.publicKeyVerify(key), 'Invalid public key.');
}
script = p.readVarBytes();

View File

@ -295,6 +295,21 @@ LRU.prototype.keys = function keys() {
return keys;
};
/**
* Collect all values in the cache, sorted by LRU.
* @returns {String[]}
*/
LRU.prototype.values = function values() {
var values = [];
var item;
for (item = this.head; item; item = item.next)
values.push(item.value);
return values;
};
/**
* Convert the LRU cache to an array of items.
* @returns {Object[]}
@ -336,12 +351,16 @@ function NullCache(size) {}
NullCache.prototype.set = function set(key, value) {};
NullCache.prototype.remove = function remove(key) {};
NullCache.prototype.get = function get(key) {};
NullCache.prototype.has = function has(key) {};
NullCache.prototype.has = function has(key) { return false; };
NullCache.prototype.reset = function reset() {};
NullCache.prototype.keys = function keys(key) { return []; };
NullCache.prototype.values = function values(key) { return []; };
NullCache.prototype.toArray = function toArray(key) { return []; };
/*
* Expose
*/
LRU.nil = NullCache;
module.exports = LRU;

View File

@ -349,12 +349,7 @@ Account.prototype._checkKeys = co(function* _checkKeys() {
ring = this.deriveReceive(0);
hash = ring.getScriptHash('hex');
paths = yield this.db.getAddressPaths(hash);
if (!paths)
return false;
return paths[this.wid] != null;
return yield this.db.hasAddress(this.wid, hash);
});
/**

View File

@ -17,6 +17,12 @@ layout.walletdb = {
pp: function(key) {
return key.slice(1);
},
P: function(wid, hash) {
return 'p' + pad32(wid) + hash;
},
Pp: function(key) {
return key.slice(11);
},
w: function(wid) {
return 'w' + pad32(wid);
},

View File

@ -30,8 +30,10 @@ function Path() {
if (!(this instanceof Path))
return new Path();
// Passed in by caller.
this.wid = null;
this.name = null; // Passed in by caller.
this.name = null;
this.account = 0;
this.change = -1;
this.index = -1;
@ -97,10 +99,7 @@ Path.prototype.clone = function clone() {
Path.prototype.fromRaw = function fromRaw(data) {
var p = new BufferReader(data);
this.wid = p.readU32();
this.account = p.readU32();
this.change = -1;
this.index = -1;
this.keyType = p.readU8();
switch (this.keyType) {
@ -144,9 +143,7 @@ Path.fromRaw = function fromRaw(data) {
Path.prototype.toRaw = function toRaw(writer) {
var p = new BufferWriter(writer);
p.writeU32(this.wid);
p.writeU32(this.account);
p.writeU8(this.keyType);
switch (this.keyType) {
@ -199,7 +196,7 @@ Path.prototype.fromHD = function fromHD(account, ring, change, index) {
this.keyType = Path.types.HD;
this.version = ring.witness ? 0 : -1;
this.type = ring.getType();
this.type = ring.getAddressType();
this.id = account.id;
this.hash = ring.getHash('hex');
@ -234,7 +231,7 @@ Path.prototype.fromKey = function fromKey(account, ring) {
this.keyType = Path.types.KEY;
this.data = ring.toRaw();
this.version = ring.witness ? 0 : -1;
this.type = ring.getType();
this.type = ring.getAddressType();
this.id = account.id;
this.hash = ring.getHash('hex');
return this;
@ -312,8 +309,9 @@ Path.prototype.toAddress = function toAddress(network) {
Path.prototype.toJSON = function toJSON() {
return {
name: this.name,
account: this.account,
change: this.change === 1,
path: this.toPath()
derivation: this.toPath()
};
};

View File

@ -341,12 +341,40 @@ TXDB.prototype.has = function has(key) {
* @returns {Promise}
*/
TXDB.prototype.iterate = function iterate(options) {
TXDB.prototype.range = function range(options) {
if (options.gte)
options.gte = this.prefix(options.gte);
if (options.lte)
options.lte = this.prefix(options.lte);
return this.db.iterate(options);
return this.db.range(options);
};
/**
* Iterate.
* @param {Object} options
* @returns {Promise}
*/
TXDB.prototype.keys = function keys(options) {
if (options.gte)
options.gte = this.prefix(options.gte);
if (options.lte)
options.lte = this.prefix(options.lte);
return this.db.keys(options);
};
/**
* Iterate.
* @param {Object} options
* @returns {Promise}
*/
TXDB.prototype.values = function values(options) {
if (options.gte)
options.gte = this.prefix(options.gte);
if (options.lte)
options.lte = this.prefix(options.lte);
return this.db.values(options);
};
/**
@ -1307,7 +1335,7 @@ TXDB.prototype.getLocked = function getLocked() {
*/
TXDB.prototype.getHistoryHashes = function getHistoryHashes(account) {
return this.iterate({
return this.keys({
gte: account != null
? layout.T(account, constants.NULL_HASH)
: layout.t(constants.NULL_HASH),
@ -1332,7 +1360,7 @@ TXDB.prototype.getHistoryHashes = function getHistoryHashes(account) {
*/
TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(account) {
return this.iterate({
return this.keys({
gte: account != null
? layout.P(account, constants.NULL_HASH)
: layout.p(constants.NULL_HASH),
@ -1357,7 +1385,7 @@ TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(account) {
*/
TXDB.prototype.getOutpoints = function getOutpoints(account) {
return this.iterate({
return this.keys({
gte: account != null
? layout.C(account, constants.NULL_HASH, 0)
: layout.c(constants.NULL_HASH, 0),
@ -1397,7 +1425,7 @@ TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(account, opt
start = options.start || 0;
end = options.end || 0xffffffff;
return this.iterate({
return this.keys({
gte: account != null
? layout.H(account, start, constants.NULL_HASH)
: layout.h(start, constants.NULL_HASH),
@ -1449,7 +1477,7 @@ TXDB.prototype.getRangeHashes = function getRangeHashes(account, options) {
start = options.start || 0;
end = options.end || 0xffffffff;
return this.iterate({
return this.keys({
gte: account != null
? layout.M(account, start, constants.NULL_HASH)
: layout.m(start, constants.NULL_HASH),
@ -1532,14 +1560,10 @@ TXDB.prototype.getHistory = function getHistory(account) {
return this.getAccountHistory(account);
// Fast case
return this.iterate({
return this.values({
gte: layout.t(constants.NULL_HASH),
lte: layout.t(constants.HIGH_HASH),
keys: false,
values: true,
parse: function(key, value) {
return bcoin.tx.fromExtended(value);
}
parse: bcoin.tx.fromExtended
});
};
@ -1607,10 +1631,9 @@ TXDB.prototype.getCoins = function getCoins(account) {
return this.getAccountCoins(account);
// Fast case
return this.iterate({
gte: layout.c(constants.NULL_HASH, 0),
return this.range({
gte: layout.c(constants.NULL_HASH, 0x00000000),
lte: layout.c(constants.HIGH_HASH, 0xffffffff),
values: true,
parse: function(key, value) {
var parts = layout.cc(key);
var hash = parts[0];
@ -1663,10 +1686,9 @@ TXDB.prototype.fillHistory = function fillHistory(tx) {
hash = tx.hash('hex');
return this.iterate({
gte: layout.d(hash, 0),
return this.range({
gte: layout.d(hash, 0x00000000),
lte: layout.d(hash, 0xffffffff),
values: true,
parse: function(key, value) {
var index = layout.dd(key)[1];
var coin = bcoin.coin.fromRaw(value);
@ -1897,10 +1919,9 @@ TXDB.prototype.getBalance = co(function* getBalance(account) {
// Fast case
balance = new Balance(this.wallet);
yield this.iterate({
gte: layout.c(constants.NULL_HASH, 0),
yield this.range({
gte: layout.c(constants.NULL_HASH, 0x00000000),
lte: layout.c(constants.HIGH_HASH, 0xffffffff),
values: true,
parse: function(key, data) {
var parts = layout.cc(key);
var hash = parts[0];

View File

@ -403,6 +403,56 @@ Wallet.prototype._retoken = co(function* retoken(passphrase) {
return this.token;
});
/**
* Rename the wallet.
* @param {String} id
* @returns {Promise}
*/
Wallet.prototype.rename = co(function* rename(id) {
var unlock = yield this.writeLock.lock();
try {
return yield this.db.rename(this, id);
} finally {
unlock();
}
});
/**
* Rename account.
* @param {(String|Number)?} account
* @param {String} name
* @returns {Promise}
*/
Wallet.prototype.renameAccount = co(function* renameAccount(account, name) {
var unlock = yield this.writeLock.lock();
try {
return yield this._renameAccount(account, name);
} finally {
unlock();
}
});
/**
* Rename account without a lock.
* @private
* @param {(String|Number)?} account
* @param {String} name
* @returns {Promise}
*/
Wallet.prototype._renameAccount = co(function* _renameAccount(account, name) {
assert(utils.isName(name), 'Bad account name.');
account = yield this.getAccount(account);
if (!account)
throw new Error('Account not found.');
yield this.db.renameAccount(account, name);
});
/**
* Lock the wallet, destroy decrypted key.
*/
@ -587,7 +637,7 @@ Wallet.prototype.getAccounts = function getAccounts() {
*/
Wallet.prototype.getAddressHashes = function getAddressHashes() {
return this.db.getAddressHashes(this.wid);
return this.db.getWalletHashes(this.wid);
};
/**
@ -759,7 +809,7 @@ Wallet.prototype.getPath = co(function* getPath(address) {
if (!hash)
return;
path = yield this.db.getAddressPath(this.wid, hash);
path = yield this.db.getPath(this.wid, hash);
if (!path)
return;

View File

@ -23,7 +23,8 @@ var MAX_POINT = String.fromCharCode(0xdbff, 0xdfff); // U+10FFFF
/*
* Database Layout:
* p[addr-hash] -> path data
* p[addr-hash] -> wallet ids
* P[wid][addr-hash] -> path data
* w[wid] -> wallet
* l[id] -> wid
* a[wid][index] -> account
@ -44,6 +45,16 @@ var layout = {
pp: function(key) {
return key.toString('hex', 1);
},
P: function(wid, hash) {
var key = new Buffer(1 + 4 + (hash.length / 2));
key[0] = 0x50;
key.writeUInt32BE(wid, 1, true);
key.write(hash, 5, 'hex');
return key;
},
Pp: function(key) {
return key.toString('hex', 5);
},
w: function(wid) {
var key = new Buffer(5);
key[0] = 0x77;
@ -140,9 +151,11 @@ function WalletDB(options) {
this.writeLock = new bcoin.locker();
this.txLock = new bcoin.locker();
this.walletCache = new bcoin.lru(10000);
this.widCache = new bcoin.lru(10000);
this.indexCache = new bcoin.lru(10000);
this.accountCache = new bcoin.lru(10000);
this.pathCache = new bcoin.lru(100000);
this.pathMapCache = new bcoin.lru(100000);
// Try to optimize for up to 1m addresses.
// We use a regular bloom filter here
@ -276,9 +289,12 @@ WalletDB.prototype.getDepth = co(function* getDepth() {
*/
WalletDB.prototype.start = function start(wid) {
var batch;
assert(utils.isNumber(wid), 'Bad ID for batch.');
assert(!this.batches[wid], 'Batch already started.');
this.batches[wid] = this.db.batch();
batch = this.db.batch();
this.batches[wid] = batch;
return batch;
};
/**
@ -327,21 +343,27 @@ WalletDB.prototype.commit = function commit(wid) {
* @returns {Promise}
*/
WalletDB.prototype.loadFilter = function loadFilter() {
var self = this;
WalletDB.prototype.loadFilter = co(function* loadFilter() {
var iter, item, hash;
if (!this.filter)
return Promise.resolve(null);
return;
return this.db.iterate({
iter = this.db.iterator({
gte: layout.p(constants.NULL_HASH),
lte: layout.p(constants.HIGH_HASH),
parse: function(key) {
var hash = layout.pp(key);
self.filter.add(hash, 'hex');
}
lte: layout.p(constants.HIGH_HASH)
});
};
for (;;) {
item = yield iter.next();
if (!item)
break;
hash = layout.pp(item.key);
this.filter.add(hash, 'hex');
}
});
/**
* Test the bloom filter against an array of address hashes.
@ -369,18 +391,9 @@ WalletDB.prototype.testFilter = function testFilter(hashes) {
* @returns {Promise} - Returns Object.
*/
WalletDB.prototype.dump = co(function* dump() {
var records = {};
yield this.db.iterate({
gte: new Buffer([0x00]),
lte: new Buffer([0xff]),
values: true,
parse: function(key, value) {
records[key.toString('hex')] = value.toString('hex');
}
});
return records;
});
WalletDB.prototype.dump = function dump() {
return this.db.dump();
};
/**
* Register an object with the walletdb.
@ -418,7 +431,7 @@ WalletDB.prototype.getWalletID = co(function* getWalletID(id) {
if (typeof id === 'number')
return id;
wid = this.walletCache.get(id);
wid = this.widCache.get(id);
if (wid)
return wid;
@ -430,7 +443,7 @@ WalletDB.prototype.getWalletID = co(function* getWalletID(id) {
wid = data.readUInt32LE(0, true);
this.walletCache.set(id, wid);
this.widCache.set(id, wid);
return wid;
});
@ -441,11 +454,10 @@ WalletDB.prototype.getWalletID = co(function* getWalletID(id) {
* @returns {Promise} - Returns {@link Wallet}.
*/
WalletDB.prototype.get = co(function* get(wid) {
WalletDB.prototype.get = co(function* get(id) {
var wid = yield this.getWalletID(id);
var unlock;
wid = yield this.getWalletID(wid);
if (!wid)
return;
@ -466,11 +478,8 @@ WalletDB.prototype.get = co(function* get(wid) {
*/
WalletDB.prototype._get = co(function* get(wid) {
var data, wallet;
// By the time the lock is released,
// the wallet may be watched.
wallet = this.wallets[wid];
var wallet = this.wallets[wid];
var data;
if (wallet)
return wallet;
@ -495,14 +504,123 @@ WalletDB.prototype._get = co(function* get(wid) {
*/
WalletDB.prototype.save = function save(wallet) {
var batch = this.batch(wallet.wid);
var wid = new Buffer(4);
this.walletCache.set(wallet.id, wallet.wid);
batch.put(layout.w(wallet.wid), wallet.toRaw());
wid.writeUInt32LE(wallet.wid, 0, true);
batch.put(layout.l(wallet.id), wid);
var wid = wallet.wid;
var id = wallet.id;
var batch = this.batch(wid);
var buf = new Buffer(4);
this.widCache.set(id, wid);
batch.put(layout.w(wid), wallet.toRaw());
buf.writeUInt32LE(wid, 0, true);
batch.put(layout.l(id), buf);
};
/**
* Rename a wallet.
* @param {Wallet} wallet
* @param {String} id
* @returns {Promise}
*/
WalletDB.prototype.rename = co(function* rename(wallet, id) {
var unlock = yield this.writeLock.lock();
try {
return yield this._rename(wallet, id);
} finally {
unlock();
}
});
/**
* Rename a wallet without a lock.
* @private
* @param {Wallet} wallet
* @param {String} id
* @returns {Promise}
*/
WalletDB.prototype._rename = co(function* _rename(wallet, id) {
var old = wallet.id;
var i, paths, path, batch;
assert(utils.isName(id), 'Bad wallet ID.');
if (yield this.has(id))
throw new Error('ID not available.');
this.widCache.remove(old);
paths = this.pathCache.values();
// TODO: Optimize this bullshit.
for (i = 0; i < paths.length; i++) {
path = paths[i];
if (path.wid !== wallet.wid)
continue;
path.id = id;
}
wallet.id = id;
batch = this.start(wallet.wid);
batch.del(layout.l(old));
this.save(wallet);
yield this.commit(wallet.wid);
});
/**
* Rename an account.
* @param {Account} account
* @param {String} name
* @returns {Promise}
*/
WalletDB.prototype.renameAccount = co(function* renameAccount(account, name) {
var old = account.name;
var key = account.wid + '/' + old;
var i, paths, path, batch;
assert(utils.isName(name), 'Bad account name.');
if (account.accountIndex === 0)
throw new Error('Cannot rename primary account.');
if (yield this.hasAccount(account.wid, name))
throw new Error('Account name not available.');
this.indexCache.remove(key);
paths = this.pathCache.values();
// TODO: Optimize this bullshit.
for (i = 0; i < paths.length; i++) {
path = paths[i];
if (path.wid !== account.wid)
continue;
if (path.account !== account.accountIndex)
continue;
path.name = name;
}
account.name = name;
batch = this.start(account.wid);
batch.del(layout.i(account.wid, old));
this.saveAccount(account);
yield this.commit(account.wid);
});
/**
* Test an api key against a wallet's api key.
* @param {WalletID} wid
@ -657,19 +775,20 @@ WalletDB.prototype._getAccount = co(function* getAccount(wid, index) {
WalletDB.prototype.getAccounts = co(function* getAccounts(wid) {
var map = [];
var i, accounts;
var i, items, item, name, index, accounts;
yield this.db.iterate({
items = yield this.db.range({
gte: layout.i(wid, ''),
lte: layout.i(wid, MAX_POINT),
values: true,
parse: function(key, value) {
var name = layout.ii(key)[1];
var index = value.readUInt32LE(0, true);
map[index] = name;
}
lte: layout.i(wid, MAX_POINT)
});
for (i = 0; i < items.length; i++) {
item = items[i];
name = layout.ii(item.key)[1];
index = item.value.readUInt32LE(0, true);
map[index] = name;
}
// Get it out of hash table mode.
accounts = [];
@ -689,7 +808,7 @@ WalletDB.prototype.getAccounts = co(function* getAccounts(wid) {
*/
WalletDB.prototype.getAccountIndex = co(function* getAccountIndex(wid, name) {
var index;
var key, index;
if (!wid)
return -1;
@ -700,12 +819,22 @@ WalletDB.prototype.getAccountIndex = co(function* getAccountIndex(wid, name) {
if (typeof name === 'number')
return name;
key = wid + '/' + name;
index = this.indexCache.get(key);
if (index != null)
return index;
index = yield this.db.get(layout.i(wid, name));
if (!index)
return -1;
return index.readUInt32LE(0, true);
index = index.readUInt32LE(0, true);
this.indexCache.set(key, index);
return index;
});
/**
@ -731,14 +860,17 @@ WalletDB.prototype.getAccountName = co(function* getAccountName(wid, index) {
*/
WalletDB.prototype.saveAccount = function saveAccount(account) {
var batch = this.batch(account.wid);
var index = new Buffer(4);
var key = account.wid + '/' + account.accountIndex;
var wid = account.wid;
var index = account.accountIndex;
var name = account.name;
var batch = this.batch(wid);
var key = wid + '/' + index;
var buf = new Buffer(4);
index.writeUInt32LE(account.accountIndex, 0, true);
buf.writeUInt32LE(index, 0, true);
batch.put(layout.a(account.wid, account.accountIndex), account.toRaw());
batch.put(layout.i(account.wid, account.name), index);
batch.put(layout.a(wid, index), account.toRaw());
batch.put(layout.i(wid, name), buf);
this.accountCache.set(key, account);
};
@ -794,10 +926,34 @@ WalletDB.prototype.hasAccount = co(function* hasAccount(wid, account) {
return yield this.db.has(layout.a(wid, index));
});
/**
* Lookup the corresponding account name's index.
* @param {WalletID} wid
* @param {String|Number} name - Account name/index.
* @returns {Promise} - Returns Number.
*/
WalletDB.prototype.getWalletsByHash = co(function* getWalletsByHash(hash) {
var wallets = this.pathMapCache.get(hash);
var data;
if (wallets)
return wallets;
data = yield this.db.get(layout.p(hash));
if (!data)
return;
wallets = parseWallets(data);
this.pathMapCache.get(hash, wallets);
return wallets;
});
/**
* Save addresses to the path map.
* The path map exists in the form of:
* `p/[address-hash] -> {walletid1=path1, walletid2=path2, ...}`
* @param {WalletID} wid
* @param {KeyRing[]} rings
* @returns {Promise}
@ -810,7 +966,7 @@ WalletDB.prototype.saveAddress = co(function* saveAddress(wid, rings) {
ring = rings[i];
path = ring.path;
yield this.writeAddress(wid, ring.getAddress(), path);
yield this.writePath(wid, path);
if (!ring.witness)
continue;
@ -820,45 +976,17 @@ WalletDB.prototype.saveAddress = co(function* saveAddress(wid, rings) {
path.version = -1;
path.type = Script.types.SCRIPTHASH;
yield this.writeAddress(wid, ring.getProgramAddress(), path);
yield this.writePath(wid, path);
}
});
/**
* Save a single address to the path map.
* @param {WalletID} wid
* @param {KeyRing} rings
* @param {Path} path
* @returns {Promise}
*/
WalletDB.prototype.writeAddress = co(function* writeAddress(wid, address, path) {
var hash = address.getHash('hex');
var batch = this.batch(wid);
var paths;
if (this.filter)
this.filter.add(hash, 'hex');
this.emit('save address', address, path);
paths = yield this.getAddressPaths(hash);
if (!paths)
paths = {};
if (paths[wid])
return;
paths[wid] = path;
this.pathCache.set(hash, paths);
batch.put(layout.p(hash), serializePaths(paths));
});
/**
* Save paths to the path map.
*
* The path map exists in the form of:
* - `p[address-hash] -> wids`
* - `P[wid][address-hash] -> path`
*
* @param {WalletID} wid
* @param {Path[]} paths
* @returns {Promise}
@ -883,26 +1011,29 @@ WalletDB.prototype.savePath = co(function* savePath(wid, paths) {
WalletDB.prototype.writePath = co(function* writePath(wid, path) {
var hash = path.hash;
var batch = this.batch(wid);
var paths;
var key = wid + hash;
var wallets;
if (this.filter)
this.filter.add(hash, 'hex');
this.emit('save address', path.toAddress(), path);
paths = yield this.getAddressPaths(hash);
wallets = yield this.getWalletsByHash(hash);
if (!paths)
paths = {};
if (!wallets)
wallets = [];
if (paths[wid])
if (wallets.indexOf(wid) !== -1)
return;
paths[wid] = path;
wallets.push(wid);
this.pathCache.set(hash, paths);
this.pathMapCache.set(hash, wallets);
this.pathCache.set(key, path);
batch.put(layout.p(hash), serializePaths(paths));
batch.put(layout.p(hash), serializeWallets(wallets));
batch.put(layout.P(wid, hash), path.toRaw());
});
/**
@ -911,47 +1042,57 @@ WalletDB.prototype.writePath = co(function* writePath(wid, path) {
* @returns {Promise}
*/
WalletDB.prototype.getAddressPaths = co(function* getAddressPaths(hash) {
var paths, data;
WalletDB.prototype.getPaths = co(function* getPaths(hash) {
var wallets = yield this.getWalletsByHash(hash);
var i, wid, path, paths;
if (!hash)
if (!wallets)
return;
paths = this.pathCache.get(hash);
paths = [];
if (paths)
return paths;
data = yield this.db.get(layout.p(hash));
if (!data)
return;
paths = parsePaths(data, hash);
yield this.fillPathNames(paths);
this.pathCache.set(hash, paths);
for (i = 0; i < wallets.length; i++) {
wid = wallets[i];
path = yield this.getPath(wid, hash);
if (path)
paths.push(path);
}
return paths;
});
/**
* Assign account names to an array of paths.
* @param {Path[]} paths
* Retrieve path by hash.
* @param {WalletID} wid
* @param {Hash} hash
* @returns {Promise}
*/
WalletDB.prototype.fillPathNames = co(function* fillPathNames(paths) {
var i, path;
WalletDB.prototype.getPath = co(function* getPath(wid, hash) {
var key, path, data;
for (i = 0; i < paths.length; i++) {
path = paths[i];
if (path.name)
continue;
// These should be mostly cached.
path.name = yield this.db.getAccountName(path.wid, path.account);
}
if (!hash)
return;
key = wid + hash;
path = this.pathCache.get(key);
if (path)
return path;
data = yield this.db.get(layout.P(wid, hash));
if (!data)
return;
path = Path.fromRaw(data);
path.wid = wid;
path.hash = hash;
path.name = yield this.getAccountName(wid, path.account);
this.pathCache.set(key, path);
return path;
});
/**
@ -963,33 +1104,34 @@ WalletDB.prototype.fillPathNames = co(function* fillPathNames(paths) {
*/
WalletDB.prototype.hasAddress = co(function* hasAddress(wid, hash) {
var paths = yield this.getAddressPaths(hash);
if (!paths || !paths[wid])
return false;
return true;
var path = yield this.getPath(wid, hash);
return path != null;
});
/**
* Get all address hashes.
* @returns {Promise}
*/
WalletDB.prototype.getHashes = function getHashes() {
return this.db.keys({
gte: layout.p(constants.NULL_HASH),
lte: layout.p(constants.HIGH_HASH),
parse: layout.pp
});
};
/**
* Get all address hashes.
* @param {WalletID} wid
* @returns {Promise}
*/
WalletDB.prototype.getAddressHashes = function getAddressHashes(wid) {
return this.db.iterate({
gte: layout.p(constants.NULL_HASH),
lte: layout.p(constants.HIGH_HASH),
values: true,
parse: function(key, value) {
var paths = parsePaths(value);
if (wid && !paths[wid])
return;
return layout.pp(key);
}
WalletDB.prototype.getWalletHashes = function getWalletHashes(wid) {
return this.db.keys({
gte: layout.P(wid, constants.NULL_HASH),
lte: layout.P(wid, constants.HIGH_HASH),
parse: layout.Pp
});
};
@ -1000,25 +1142,26 @@ WalletDB.prototype.getAddressHashes = function getAddressHashes(wid) {
*/
WalletDB.prototype.getWalletPaths = co(function* getWalletPaths(wid) {
var paths = yield this.db.iterate({
gte: layout.p(constants.NULL_HASH),
lte: layout.p(constants.HIGH_HASH),
values: true,
parse: function(key, value) {
var hash = layout.pp(key);
var paths = parsePaths(value, hash);
var path = paths[wid];
var i, item, items, hash, path;
if (!path)
return;
return path;
}
items = yield this.db.range({
gte: layout.P(wid, constants.NULL_HASH),
lte: layout.P(wid, constants.HIGH_HASH)
});
yield this.fillPathNames(paths);
for (i = 0; i < items.length; i++) {
item = items[i];
hash = layout.Pp(item.key);
path = Path.fromRaw(item.value);
return paths;
path.hash = hash;
path.wid = wid;
path.name = yield this.getAccountName(wid, path.account);
items[i] = path;
}
return items;
});
/**
@ -1027,12 +1170,10 @@ WalletDB.prototype.getWalletPaths = co(function* getWalletPaths(wid) {
*/
WalletDB.prototype.getWallets = function getWallets() {
return this.db.iterate({
return this.db.keys({
gte: layout.l(''),
lte: layout.l(MAX_POINT),
parse: function(key) {
return layout.ll(key);
}
parse: layout.ll
});
};
@ -1067,7 +1208,7 @@ WalletDB.prototype._rescan = co(function* rescan(chaindb, height) {
if (height == null)
height = this.height;
hashes = yield this.getAddressHashes();
hashes = yield this.getHashes();
this.logger.info('Scanning for %d addresses.', hashes.length);
@ -1082,33 +1223,46 @@ WalletDB.prototype._rescan = co(function* rescan(chaindb, height) {
* @returns {Promise}
*/
WalletDB.prototype.getPendingKeys = function getPendingKeys() {
WalletDB.prototype.getPendingKeys = co(function* getPendingKeys() {
var layout = require('./txdb').layout;
var dummy = new Buffer(0);
var uniq = {};
var keys = [];
var result = [];
var i, iter, item, key, wid, hash;
return this.db.iterate({
iter = yield this.db.iterator({
gte: layout.prefix(0x00000000, dummy),
lte: layout.prefix(0xffffffff, dummy),
keys: true,
parse: function(key) {
var wid, hash;
if (key[5] !== 0x70)
return;
wid = layout.pre(key);
hash = layout.pp(key);
if (uniq[hash])
return;
uniq[hash] = true;
return layout.prefix(wid, layout.t(hash));
}
lte: layout.prefix(0xffffffff, dummy)
});
};
for (;;) {
item = yield iter.next();
if (!item)
break;
if (item.key[5] === 0x70)
keys.push(item.key);
}
for (i = 0; i < keys.length; i++) {
key = keys[i];
wid = layout.pre(key);
hash = layout.pp(key);
if (uniq[hash])
continue;
uniq[hash] = true;
key = layout.prefix(wid, layout.t(hash));
result.push(key);
}
return result;
});
/**
* Resend all pending transactions.
@ -1189,22 +1343,14 @@ WalletDB.prototype.getTable = co(function* getTable(hashes) {
for (i = 0; i < hashes.length; i++) {
hash = hashes[i];
paths = yield this.getAddressPaths(hash);
paths = yield this.getPaths(hash);
if (!paths) {
assert(!table[hash]);
table[hash] = [];
continue;
}
keys = Object.keys(paths);
values = [];
for (j = 0; j < keys.length; j++)
values.push(paths[keys[j]]);
assert(!table[hash]);
table[hash] = values;
table[hash] = paths;
match = true;
}
@ -1522,28 +1668,6 @@ WalletDB.prototype._addTX = co(function* addTX(tx, force) {
return wallets;
});
/**
* Get the corresponding path for an address hash.
* @param {WalletID} wid
* @param {Hash} hash
* @returns {Promise}
*/
WalletDB.prototype.getAddressPath = co(function* getAddressPath(wid, hash) {
var paths = yield this.getAddressPaths(hash);
var path;
if (!paths)
return;
path = paths[wid];
if (!path)
return;
return path;
});
/**
* Path Info
* @constructor
@ -1924,35 +2048,6 @@ WalletBlock.prototype.toJSON = function toJSON() {
* Helpers
*/
function parsePaths(data, hash) {
var p = new BufferReader(data);
var out = {};
var path;
while (p.left()) {
path = Path.fromRaw(p);
out[path.wid] = path;
if (hash)
path.hash = hash;
}
return out;
}
function serializePaths(out) {
var p = new BufferWriter();
var keys = Object.keys(out);
var i, wid, path;
for (i = 0; i < keys.length; i++) {
wid = keys[i];
path = out[wid];
path.toRaw(p);
}
return p.render();
}
function parseWallets(data) {
var p = new BufferReader(data);
var wallets = [];

View File

@ -953,6 +953,14 @@ describe('Wallet', function() {
assert.equal(details[0].toJSON().outputs[0].path.name, 'foo');
}));
it('should rename wallet', cob(function *() {
var w = wallet;
yield wallet.rename('test');
var txs = yield w.getRange('foo', { start: 0xdeadbeef - 1000 });
var details = yield w.toDetails(txs);
assert.equal(details[0].toJSON().id, 'test');
}));
it('should cleanup', cob(function *() {
var records = yield walletdb.dump();
constants.tx.COINBASE_MATURITY = 100;