diff --git a/lib/chain/chaindb.js b/lib/chain/chaindb.js index 9e03e5c3..519d1e2c 100644 --- a/lib/chain/chaindb.js +++ b/lib/chain/chaindb.js @@ -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) { diff --git a/lib/db/lowlevelup.js b/lib/db/lowlevelup.js index 7d43d81a..0f190ff1 100644 --- a/lib/db/lowlevelup.js +++ b/lib/db/lowlevelup.js @@ -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 diff --git a/lib/primitives/keyring.js b/lib/primitives/keyring.js index 903ebabd..a388962a 100644 --- a/lib/primitives/keyring.js +++ b/lib/primitives/keyring.js @@ -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(); diff --git a/lib/utils/lru.js b/lib/utils/lru.js index b699d10b..55ea9984 100644 --- a/lib/utils/lru.js +++ b/lib/utils/lru.js @@ -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; diff --git a/lib/wallet/account.js b/lib/wallet/account.js index 205aa672..37b17b8b 100644 --- a/lib/wallet/account.js +++ b/lib/wallet/account.js @@ -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); }); /** diff --git a/lib/wallet/browser.js b/lib/wallet/browser.js index bf16f2e0..b2d7dcd7 100644 --- a/lib/wallet/browser.js +++ b/lib/wallet/browser.js @@ -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); }, diff --git a/lib/wallet/path.js b/lib/wallet/path.js index 062231e2..8883c351 100644 --- a/lib/wallet/path.js +++ b/lib/wallet/path.js @@ -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() }; }; diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 8027f3cc..dee978f5 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -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]; diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index b000f3b4..ecd3f92a 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -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; diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 7d6e58ff..c4e88f71 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -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 = []; diff --git a/test/wallet-test.js b/test/wallet-test.js index fda77f77..1d26acd4 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -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;