From 55f5ff94932a36ab3df538f65239cdc3e7aac77a Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Mon, 16 Oct 2017 18:25:07 -0700 Subject: [PATCH] wallet: use batches. --- lib/db/lowlevelup.js | 374 ++++++++++++++--- lib/http/client.js | 6 +- lib/http/wallet.js | 4 +- lib/wallet/account.js | 83 ++-- lib/wallet/http.js | 40 +- lib/wallet/layout-browser.js | 38 +- lib/wallet/layout.js | 52 +-- lib/wallet/rpc.js | 8 +- lib/wallet/txdb.js | 788 ++++++++++++++--------------------- lib/wallet/wallet.js | 472 +++++---------------- lib/wallet/walletdb.js | 331 ++++++--------- test/wallet-test.js | 28 +- 12 files changed, 963 insertions(+), 1261 deletions(-) diff --git a/lib/db/lowlevelup.js b/lib/db/lowlevelup.js index 547219c5..e0809f32 100644 --- a/lib/db/lowlevelup.js +++ b/lib/db/lowlevelup.js @@ -11,6 +11,7 @@ const assert = require('assert'); const LOW = Buffer.from([0x00]); const HIGH = Buffer.from([0xff]); +const DUMMY = Buffer.alloc(0); let VERSION_ERROR; @@ -50,7 +51,6 @@ function LowlevelUp(backend, location, options) { /** * Initialize the database. - * @method * @private */ @@ -293,6 +293,19 @@ LowlevelUp.prototype.batch = function batch() { return new Batch(this); }; +/** + * Create a bucket. + * @param {Buffer} [prefix=DUMMY] + * @returns {Bucket} + */ + +LowlevelUp.prototype.bucket = function bucket(prefix) { + if (!this.loaded) + throw new Error('Database is closed.'); + + return new Bucket(this, this.batch(), prefix); +}; + /** * Create an iterator. * @param {Object} options @@ -376,7 +389,6 @@ LowlevelUp.prototype.compactRange = function compactRange(start, end) { /** * Test whether a key exists. - * @method * @param {String} key * @returns {Promise} - Returns Boolean. */ @@ -388,89 +400,54 @@ LowlevelUp.prototype.has = async function has(key) { /** * Collect all keys from iterator options. - * @method * @param {Object} options - Iterator options. * @returns {Promise} - Returns Array. */ -LowlevelUp.prototype.range = async function range(options) { +LowlevelUp.prototype.range = function range(options) { const iter = this.iterator({ gte: options.gte, lte: options.lte, keys: true, values: true }); - - const items = []; - - await iter.each((key, value) => { - if (options.parse) { - const item = options.parse(key, value); - if (item) - items.push(item); - } else { - items.push(new IteratorItem(key, value)); - } - }); - - return items; + return iter.range(options.parse); }; /** * Collect all keys from iterator options. - * @method * @param {Object} options - Iterator options. * @returns {Promise} - Returns Array. */ -LowlevelUp.prototype.keys = async function keys(options) { +LowlevelUp.prototype.keys = function keys(options) { const iter = this.iterator({ gte: options.gte, lte: options.lte, keys: true, values: false }); - - const items = []; - - await iter.each((key) => { - if (options.parse) - key = options.parse(key); - items.push(key); - }); - - return items; + return iter.keys(options.parse); }; /** * Collect all keys from iterator options. - * @method * @param {Object} options - Iterator options. * @returns {Promise} - Returns Array. */ -LowlevelUp.prototype.values = async function values(options) { +LowlevelUp.prototype.values = function values(options) { const iter = this.iterator({ gte: options.gte, lte: options.lte, keys: false, values: true }); - - const items = []; - - await iter.each((value) => { - if (options.parse) - value = options.parse(value); - items.push(value); - }); - - return items; + return iter.values(options.parse); }; /** * Dump database (for debugging). - * @method * @returns {Promise} - Returns Object. */ @@ -493,7 +470,6 @@ LowlevelUp.prototype.dump = async function dump() { /** * Write and assert a version number for the database. - * @method * @param {Number} version * @returns {Promise} */ @@ -518,7 +494,6 @@ LowlevelUp.prototype.checkVersion = async function checkVersion(key, version) { /** * Clone the database. - * @method * @param {String} path * @returns {Promise} */ @@ -631,6 +606,168 @@ Batch.prototype.clear = function clear() { return this; }; +/** + * Bucket + * @constructor + * @ignore + * @param {LowlevelUp} db + * @param {Batch} batch + * @param {Buffer} [prefix=DUMMY] + */ + +function Bucket(db, batch, prefix, parent) { + this.db = db; + this.batch = batch; + this.prefix = prefix || DUMMY; + this.parent = parent || DUMMY; +} + +/** + * Get child bucket. + * @param {Buffer} [prefix=DUMMY] + */ + +Bucket.prototype.batch = function batch() { + return new Bucket(this.db, this.batch, this.prefix, this.parent); +}; + +/** + * Get child bucket. + * @param {Buffer} [prefix=DUMMY] + */ + +Bucket.prototype.bucket = function bucket(prefix = DUMMY) { + const parent = this.prefix; + const child = concat(parent, prefix); + return new Bucket(this.db, this.batch, child, parent); +}; + +/** + * Get child bucket. + * @param {Buffer} [prefix=DUMMY] + */ + +Bucket.prototype.up = function up() { + return new Bucket(this.db, this.batch, this.parent); +}; + +/** + * Get a value from the bucket. + * @param {String|Buffer} key + * @returns {Promise} + */ + +Bucket.prototype.has = function has(key) { + return this.db.has(concat(this.prefix, key)); +}; + +/** + * Get a value from the bucket. + * @param {String|Buffer} key + * @returns {Promise} + */ + +Bucket.prototype.get = function get(key) { + return this.db.get(concat(this.prefix, key)); +}; + +/** + * Create an iterator. + * @param {Object} options + * @returns {Iterator} + */ + +Bucket.prototype.iterator = function iterator(options) { + return new Iterator(this.db, options, this.prefix); +}; + +/** + * Collect all keys from iterator options. + * @param {Object} options - Iterator options. + * @returns {Promise} - Returns Array. + */ + +Bucket.prototype.range = function range(options = {}) { + const iter = this.iterator({ + gte: options.gte, + lte: options.lte, + keys: true, + values: true + }); + return iter.range(options.parse); +}; + +/** + * Collect all keys from iterator options. + * @param {Object} options - Iterator options. + * @returns {Promise} - Returns Array. + */ + +Bucket.prototype.keys = function keys(options = {}) { + const iter = this.iterator({ + gte: options.gte, + lte: options.lte, + keys: true, + values: false + }); + return iter.keys(options.parse); +}; + +/** + * Collect all keys from iterator options. + * @param {Object} options - Iterator options. + * @returns {Promise} - Returns Array. + */ + +Bucket.prototype.values = function values(options = {}) { + const iter = this.iterator({ + gte: options.gte, + lte: options.lte, + keys: false, + values: true + }); + return iter.values(options.parse); +}; + +/** + * Write a value to the bucket. + * @param {String|Buffer} key + * @param {Buffer} value + */ + +Bucket.prototype.put = function put(key, value) { + this.batch.put(concat(this.prefix, key), value); + return this; +}; + +/** + * Delete a value from the bucket. + * @param {String|Buffer} key + */ + +Bucket.prototype.del = function del(key) { + this.batch.del(concat(this.prefix, key)); + return this; +}; + +/** + * Write batch to database. + * @returns {Promise} + */ + +Bucket.prototype.write = function write() { + return this.batch.write(); +}; + +/** + * Clear the batch. + */ + +Bucket.prototype.clear = function clear() { + this.batch.clear(); + return this; +}; + /** * Iterator * @constructor @@ -639,8 +776,9 @@ Batch.prototype.clear = function clear() { * @param {Object} options */ -function Iterator(db, options) { - this.options = new IteratorOptions(options); +function Iterator(db, options, prefix) { + this.prefix = prefix || DUMMY; + this.options = new IteratorOptions(options, prefix); this.options.keyAsBuffer = db.options.bufferKeys; this.iter = db.binding.iterator(this.options); @@ -681,7 +819,7 @@ Iterator.prototype.each = async function each(cb) { await this.read(); while (this.cache.length > 0) { - const key = this.cache.pop(); + const key = slice(this.prefix, this.cache.pop()); const value = this.cache.pop(); let result = null; @@ -725,7 +863,7 @@ Iterator.prototype.next = async function next() { } if (this.cache.length > 0) { - this.key = this.cache.pop(); + this.key = slice(this.prefix, this.cache.pop()); this.value = this.cache.pop(); return true; } @@ -803,6 +941,64 @@ Iterator.prototype.end = function end() { }); }; +/** + * Collect all keys from iterator options. + * @param {Object} options - Iterator options. + * @returns {Promise} - Returns Array. + */ + +Iterator.prototype.range = async function range(parse) { + const items = []; + + await this.each((key, value) => { + if (parse) { + const item = parse(key, value); + if (item) + items.push(item); + } else { + items.push(new IteratorItem(key, value)); + } + }); + + return items; +}; + +/** + * Collect all keys from iterator options. + * @param {Object} options - Iterator options. + * @returns {Promise} - Returns Array. + */ + +Iterator.prototype.keys = async function keys(parse) { + const items = []; + + await this.each((key) => { + if (parse) + key = parse(key); + items.push(key); + }); + + return items; +}; + +/** + * Collect all keys from iterator options. + * @param {Object} options - Iterator options. + * @returns {Promise} - Returns Array. + */ + +Iterator.prototype.values = async function values(parse) { + const items = []; + + await this.each((value) => { + if (parse) + value = parse(value); + items.push(value); + }); + + return items; +}; + /** * Iterator Item * @ignore @@ -948,7 +1144,7 @@ LLUOptions.prototype.fromOptions = function fromOptions(options) { * @param {Object} options */ -function IteratorOptions(options) { +function IteratorOptions(options, prefix) { this.gte = null; this.lte = null; this.gt = null; @@ -964,8 +1160,7 @@ function IteratorOptions(options) { // Note: do not add this property. // this.limit = null; - if (options) - this.fromOptions(options); + this.fromOptions(options || {}, prefix || DUMMY); } /** @@ -975,27 +1170,44 @@ function IteratorOptions(options) { * @returns {IteratorOptions} */ -IteratorOptions.prototype.fromOptions = function fromOptions(options) { +IteratorOptions.prototype.fromOptions = function fromOptions(options, prefix) { assert(options, 'Options are required.'); if (options.gte != null) { assert(Buffer.isBuffer(options.gte) || typeof options.gte === 'string'); - this.gte = options.gte; + this.gte = concat(prefix, options.gte); } if (options.lte != null) { assert(Buffer.isBuffer(options.lte) || typeof options.lte === 'string'); - this.lte = options.lte; + this.lte = concat(prefix, options.lte); } if (options.gt != null) { assert(Buffer.isBuffer(options.gt) || typeof options.gt === 'string'); - this.gt = options.gt; + this.gt = concat(prefix, options.gt); } if (options.lt != null) { assert(Buffer.isBuffer(options.lt) || typeof options.lt === 'string'); - this.lt = options.lt; + this.lt = concat(prefix, options.lt); + } + + if (prefix.length > 0) { + if (!this.gt && !this.gte) + this.gt = prefix; + + if (!this.lt && !this.lte) { + const pre = Buffer.from(prefix); + for (let i = pre.length - 1; i >= 0; i--) { + if (pre[i] !== 0xff) { + pre[i] += 1; + break; + } + pre[i] = 0; + } + this.lt = pre; + } } if (options.keys != null) { @@ -1058,6 +1270,54 @@ function wrap(resolve, reject) { }; } +function slice(prefix, key) { + if (!key || key.length === 0) + return key; + + if (prefix.length === 0) + return key; + + if (typeof key === 'string') { + if (Buffer.isBuffer(prefix)) + prefix = prefix.toString('ascii'); + assert(typeof prefix === 'string'); + assert(key.length > prefix.length); + return key.slice(prefix.length); + } + + assert(Buffer.isBuffer(key)); + assert(key.length > prefix.length); + + return key.slice(prefix.length); +} + +function concat(prefix, key) { + if (prefix.length === 0) + return key; + + if (typeof key === 'string') { + if (Buffer.isBuffer(prefix)) + prefix = prefix.toString('ascii'); + assert(typeof prefix === 'string'); + return prefix + key; + } + + assert(Buffer.isBuffer(key)); + + const data = Buffer.allocUnsafe(prefix.length + key.length); + + if (typeof prefix === 'string') { + data.write(prefix, 0, 'ascii'); + } else { + assert(Buffer.isBuffer(prefix)); + prefix.copy(data, 0); + } + + key.copy(data, prefix.length); + + return data; +} + VERSION_ERROR = 'Warning:' + ' Your database does not match the current database version.' + ' This is likely because the database layout or serialization' diff --git a/lib/http/client.js b/lib/http/client.js index abb9f593..d59cfd4f 100644 --- a/lib/http/client.js +++ b/lib/http/client.js @@ -667,13 +667,13 @@ HTTPClient.prototype.retoken = async function retoken(id, passphrase) { /** * Change or set master key's passphrase. + * @param {String|Buffer} passphrase * @param {(String|Buffer)?} old - * @param {String|Buffer} new_ * @returns {Promise} */ -HTTPClient.prototype.setPassphrase = function setPassphrase(id, old, new_) { - const body = { old: old, new: new_ }; +HTTPClient.prototype.setPassphrase = function setPassphrase(id, passphrase, old) { + const body = { passphrase , old }; return this._post(`/wallet/${id}/passphrase`, body); }; diff --git a/lib/http/wallet.js b/lib/http/wallet.js index 2f5d1594..277f8158 100644 --- a/lib/http/wallet.js +++ b/lib/http/wallet.js @@ -336,8 +336,8 @@ HTTPWallet.prototype.createNested = function createNested(account) { * @see Wallet#setPassphrase */ -HTTPWallet.prototype.setPassphrase = function setPassphrase(old, new_) { - return this.client.setPassphrase(this.id, old, new_); +HTTPWallet.prototype.setPassphrase = function setPassphrase(passphrase, old) { + return this.client.setPassphrase(this.id, passphrase, old); }; /** diff --git a/lib/wallet/account.js b/lib/wallet/account.js index b0f83804..8e7f530e 100644 --- a/lib/wallet/account.js +++ b/lib/wallet/account.js @@ -48,7 +48,6 @@ function Account(wdb, options) { this.wdb = wdb; this.network = wdb.network; - this.wallet = null; this.receive = null; this.change = null; @@ -226,11 +225,11 @@ Account.MAX_LOOKAHEAD = 40; * @returns {Promise} */ -Account.prototype.init = async function init() { +Account.prototype.init = async function init(b) { // Waiting for more keys. if (this.keys.length !== this.n - 1) { assert(!this.initialized); - this.save(); + this.save(b); return; } @@ -240,7 +239,7 @@ Account.prototype.init = async function init() { this.initialized = true; - await this.initDepth(); + await this.initDepth(b); }; /** @@ -341,7 +340,7 @@ Account.prototype.spliceKey = function spliceKey(key) { * @returns {Promise} */ -Account.prototype.addSharedKey = async function addSharedKey(key) { +Account.prototype.addSharedKey = async function addSharedKey(b, key) { const result = this.pushKey(key); if (await this.hasDuplicate()) { @@ -350,7 +349,7 @@ Account.prototype.addSharedKey = async function addSharedKey(key) { } // Try to initialize again. - await this.init(); + await this.init(b); return result; }; @@ -361,14 +360,14 @@ Account.prototype.addSharedKey = async function addSharedKey(key) { * @returns {Promise} */ -Account.prototype.hasDuplicate = function hasDuplicate() { +Account.prototype.hasDuplicate = async function hasDuplicate() { if (this.keys.length !== this.n - 1) return false; const ring = this.deriveReceive(0); const hash = ring.getScriptHash('hex'); - return this.wallet.hasAddress(hash); + return this.wdb.hasPath(this.wid, hash); }; /** @@ -378,13 +377,13 @@ Account.prototype.hasDuplicate = function hasDuplicate() { * @returns {Promise} */ -Account.prototype.removeSharedKey = function removeSharedKey(key) { +Account.prototype.removeSharedKey = function removeSharedKey(b, key) { const result = this.spliceKey(key); if (!result) return false; - this.save(); + this.save(b); return true; }; @@ -422,28 +421,28 @@ Account.prototype.createNested = function createNested() { * @returns {Promise} - Returns {@link WalletKey}. */ -Account.prototype.createKey = async function createKey(branch) { +Account.prototype.createKey = async function createKey(b, branch) { let key, lookahead; switch (branch) { case 0: key = this.deriveReceive(this.receiveDepth); lookahead = this.deriveReceive(this.receiveDepth + this.lookahead); - await this.saveKey(lookahead); + await this.saveKey(b, lookahead); this.receiveDepth++; this.receive = key; break; case 1: key = this.deriveChange(this.changeDepth); lookahead = this.deriveReceive(this.changeDepth + this.lookahead); - await this.saveKey(lookahead); + await this.saveKey(b, lookahead); this.changeDepth++; this.change = key; break; case 2: key = this.deriveNested(this.nestedDepth); lookahead = this.deriveNested(this.nestedDepth + this.lookahead); - await this.saveKey(lookahead); + await this.saveKey(b, lookahead); this.nestedDepth++; this.nested = key; break; @@ -570,8 +569,8 @@ Account.prototype.deriveKey = function deriveKey(branch, index, master) { * @returns {Promise} */ -Account.prototype.save = function save() { - return this.wdb.saveAccount(this); +Account.prototype.save = function save(b) { + return this.wdb.saveAccount(b, this); }; /** @@ -580,8 +579,8 @@ Account.prototype.save = function save() { * @returns {Promise} */ -Account.prototype.saveKey = function saveKey(ring) { - return this.wdb.saveKey(this.wallet, ring); +Account.prototype.saveKey = function saveKey(b, ring) { + return this.wdb.saveKey(b, this.wid, ring); }; /** @@ -590,8 +589,8 @@ Account.prototype.saveKey = function saveKey(ring) { * @returns {Promise} */ -Account.prototype.savePath = function savePath(path) { - return this.wdb.savePath(this.wallet, path); +Account.prototype.savePath = function savePath(b, path) { + return this.wdb.savePath(b, this.wid, path); }; /** @@ -599,29 +598,29 @@ Account.prototype.savePath = function savePath(path) { * @returns {Promise} */ -Account.prototype.initDepth = async function initDepth() { +Account.prototype.initDepth = async function initDepth(b) { // Receive Address this.receive = this.deriveReceive(0); this.receiveDepth = 1; - await this.saveKey(this.receive); + await this.saveKey(b, this.receive); // Lookahead for (let i = 0; i < this.lookahead; i++) { const key = this.deriveReceive(i + 1); - await this.saveKey(key); + await this.saveKey(b, key); } // Change Address this.change = this.deriveChange(0); this.changeDepth = 1; - await this.saveKey(this.change); + await this.saveKey(b, this.change); // Lookahead for (let i = 0; i < this.lookahead; i++) { const key = this.deriveChange(i + 1); - await this.saveKey(key); + await this.saveKey(b, key); } // Nested Address @@ -629,16 +628,16 @@ Account.prototype.initDepth = async function initDepth() { this.nested = this.deriveNested(0); this.nestedDepth = 1; - await this.saveKey(this.nested); + await this.saveKey(b, this.nested); // Lookahead for (let i = 0; i < this.lookahead; i++) { const key = this.deriveNested(i + 1); - await this.saveKey(key); + await this.saveKey(b, key); } } - this.save(); + this.save(b); }; /** @@ -649,7 +648,7 @@ Account.prototype.initDepth = async function initDepth() { * @returns {Promise} - Returns {@link WalletKey}. */ -Account.prototype.syncDepth = async function syncDepth(receive, change, nested) { +Account.prototype.syncDepth = async function syncDepth(b, receive, change, nested) { let derived = false; let result = null; @@ -660,7 +659,7 @@ Account.prototype.syncDepth = async function syncDepth(receive, change, nested) for (let i = depth; i < receive + this.lookahead; i++) { const key = this.deriveReceive(i); - await this.saveKey(key); + await this.saveKey(b, key); } this.receive = this.deriveReceive(receive - 1); @@ -677,7 +676,7 @@ Account.prototype.syncDepth = async function syncDepth(receive, change, nested) for (let i = depth; i < change + this.lookahead; i++) { const key = this.deriveChange(i); - await this.saveKey(key); + await this.saveKey(b, key); } this.change = this.deriveChange(change - 1); @@ -693,7 +692,7 @@ Account.prototype.syncDepth = async function syncDepth(receive, change, nested) for (let i = depth; i < nested + this.lookahead; i++) { const key = this.deriveNested(i); - await this.saveKey(key); + await this.saveKey(b, key); } this.nested = this.deriveNested(nested - 1); @@ -704,7 +703,7 @@ Account.prototype.syncDepth = async function syncDepth(receive, change, nested) } if (derived) - this.save(); + this.save(b); return result; }; @@ -715,13 +714,9 @@ Account.prototype.syncDepth = async function syncDepth(receive, change, nested) * @returns {Promise} */ -Account.prototype.setLookahead = async function setLookahead(lookahead) { - if (lookahead === this.lookahead) { - this.wdb.logger.warning( - 'Lookahead is not changing for: %s/%s.', - this.id, this.name); +Account.prototype.setLookahead = async function setLookahead(b, lookahead) { + if (lookahead === this.lookahead) return; - } if (lookahead < this.lookahead) { const diff = this.lookahead - lookahead; @@ -739,7 +734,7 @@ Account.prototype.setLookahead = async function setLookahead(lookahead) { this.lookahead = lookahead; - this.save(); + this.save(b); return; } @@ -750,7 +745,7 @@ Account.prototype.setLookahead = async function setLookahead(lookahead) { for (let i = depth; i < target; i++) { const key = this.deriveReceive(i); - await this.saveKey(key); + await this.saveKey(b, key); } } @@ -760,7 +755,7 @@ Account.prototype.setLookahead = async function setLookahead(lookahead) { for (let i = depth; i < target; i++) { const key = this.deriveChange(i); - await this.saveKey(key); + await this.saveKey(b, key); } } @@ -770,12 +765,12 @@ Account.prototype.setLookahead = async function setLookahead(lookahead) { for (let i = depth; i < target; i++) { const key = this.deriveNested(i); - await this.saveKey(key); + await this.saveKey(b, key); } } this.lookahead = lookahead; - this.save(); + this.save(b); }; /** diff --git a/lib/wallet/http.js b/lib/wallet/http.js index b70d2ee7..dbcc2b7e 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -305,12 +305,12 @@ HTTPServer.prototype.initRouter = function initRouter() { // Change passphrase this.post('/:id/passphrase', async (req, res) => { const valid = req.valid(); + const passphrase = valid.str('passphrase'); const old = valid.str('old'); - const new_ = valid.str('new'); enforce(old || new_, 'Passphrase is required.'); - await req.wallet.setPassphrase(old, new_); + await req.wallet.setPassphrase(passphrase, old); res.send(200, { success: true }); }); @@ -416,7 +416,7 @@ HTTPServer.prototype.initRouter = function initRouter() { const details = await req.wallet.getDetails(tx.hash('hex')); - res.send(200, details.toJSON()); + res.send(200, details.toJSON(this.network)); }); // Create TX @@ -790,7 +790,7 @@ HTTPServer.prototype.initRouter = function initRouter() { const details = await req.wallet.toDetails(tx); - res.send(200, details.toJSON()); + res.send(200, details.toJSON(this.network)); }); // Resend @@ -813,38 +813,38 @@ HTTPServer.prototype.initSockets = function initSockets() { this.handleSocket(socket); }); - this.walletdb.on('tx', (id, tx, details) => { - const json = details.toJSON(); - this.to(`w:${id}`, 'wallet tx', json); + this.walletdb.on('tx', (w, tx, details) => { + const json = details.toJSON(this.network); + this.to(`w:${w.id}`, 'wallet tx', json); }); - this.walletdb.on('confirmed', (id, tx, details) => { - const json = details.toJSON(); - this.to(`w:${id}`, 'wallet confirmed', json); + this.walletdb.on('confirmed', (w, tx, details) => { + const json = details.toJSON(this.network); + this.to(`w:${w.id}`, 'wallet confirmed', json); }); - this.walletdb.on('unconfirmed', (id, tx, details) => { - const json = details.toJSON(); - this.to(`w:${id}`, 'wallet unconfirmed', json); + this.walletdb.on('unconfirmed', (w, tx, details) => { + const json = details.toJSON(this.network); + this.to(`w:${w.id}`, 'wallet unconfirmed', json); }); - this.walletdb.on('conflict', (id, tx, details) => { - const json = details.toJSON(); - this.to(`w:${id}`, 'wallet conflict', json); + this.walletdb.on('conflict', (w, tx, details) => { + const json = details.toJSON(this.network); + this.to(`w:${w.id}`, 'wallet conflict', json); }); - this.walletdb.on('balance', (id, balance) => { + this.walletdb.on('balance', (w, balance) => { const json = balance.toJSON(); - this.to(`w:${id}`, 'wallet balance', json); + this.to(`w:${w.id}`, 'wallet balance', json); }); - this.walletdb.on('address', (id, receive) => { + this.walletdb.on('address', (w, receive) => { const json = []; for (const addr of receive) json.push(addr.toJSON()); - this.to(`w:${id}`, 'wallet address', json); + this.to(`w:${w.id}`, 'wallet address', json); }); }; diff --git a/lib/wallet/layout-browser.js b/lib/wallet/layout-browser.js index f0c47c89..dd285dce 100644 --- a/lib/wallet/layout-browser.js +++ b/lib/wallet/layout-browser.js @@ -95,13 +95,8 @@ layouts.walletdb = { layouts.txdb = { binary: false, - prefix: function prefix(wid, key) { - assert(typeof key === 'string'); - return 't' + pad32(wid) + key; - }, - pre: function pre(key) { - assert(typeof key === 'string'); - return parseInt(key.slice(1, 11), 10); + prefix: function prefix(wid) { + return 't' + pad32(wid); }, R: 'R', hi: function hi(ch, hash, index) { @@ -110,8 +105,7 @@ layouts.txdb = { }, hii: function hii(key) { assert(typeof key === 'string'); - key = key.slice(12); - return [key.slice(0, 64), parseInt(key.slice(64), 10)]; + return [key.slice(1, 65), parseInt(key.slice(65), 10)]; }, ih: function ih(ch, index, hash) { assert(typeof hash === 'string'); @@ -119,8 +113,7 @@ layouts.txdb = { }, ihh: function ihh(key) { assert(typeof key === 'string'); - key = key.slice(12); - return [parseInt(key.slice(0, 10), 10), key.slice(10)]; + return [parseInt(key.slice(1, 11), 10), key.slice(11)]; }, iih: function iih(ch, index, num, hash) { assert(typeof hash === 'string'); @@ -128,11 +121,10 @@ layouts.txdb = { }, iihh: function iihh(key) { assert(typeof key === 'string'); - key = key.slice(12); return [ - parseInt(key.slice(0, 10), 10), - parseInt(key.slice(10, 20), 10), - key.slice(20) + parseInt(key.slice(1, 11), 10), + parseInt(key.slice(11, 21), 10), + key.slice(21) ]; }, ihi: function ihi(ch, index, hash, num) { @@ -141,11 +133,10 @@ layouts.txdb = { }, ihii: function ihii(key) { assert(typeof key === 'string'); - key = key.slice(12); return [ - parseInt(key.slice(0, 10), 10), - key.slice(10, 74), - parseInt(key.slice(74), 10) + parseInt(key.slice(1, 11), 10), + key.slice(11, 75), + parseInt(key.slice(75), 10) ]; }, ha: function ha(ch, hash) { @@ -154,8 +145,7 @@ layouts.txdb = { }, haa: function haa(key) { assert(typeof key === 'string'); - key = key.slice(12); - return key; + return key.slice(1); }, t: function t(hash) { return this.ha('t', hash); @@ -235,15 +225,11 @@ layouts.txdb = { Cc: function Cc(key) { return this.ihii(key); }, - r: function r(hash) { - return this.ha('r', hash); - }, b: function b(height) { return 'b' + pad32(height); }, bb: function bb(key) { assert(typeof key === 'string'); - key = key.slice(12); - return parseInt(key.slice(0), 10); + return parseInt(key.slice(1), 10); } }; diff --git a/lib/wallet/layout.js b/lib/wallet/layout.js index 4dc42e42..84819b3e 100644 --- a/lib/wallet/layout.js +++ b/lib/wallet/layout.js @@ -199,20 +199,13 @@ layouts.walletdb = { layouts.txdb = { binary: true, - prefix: function prefix(wid, key) { + prefix: function prefix(wid) { assert(typeof wid === 'number'); - assert(Buffer.isBuffer(key)); - const out = Buffer.allocUnsafe(5 + key.length); + const out = Buffer.allocUnsafe(5); out[0] = 0x74; out.writeUInt32BE(wid, 1); - key.copy(out, 5); return out; }, - pre: function pre(key) { - assert(Buffer.isBuffer(key)); - assert(key.length >= 5); - return key.readUInt32BE(1, true); - }, R: Buffer.from([0x52]), hi: function hi(ch, hash, index) { assert(typeof hash === 'string'); @@ -225,9 +218,8 @@ layouts.txdb = { }, hii: function hii(key) { assert(Buffer.isBuffer(key)); - assert(key.length - 5 === 37); - key = key.slice(6); - return [key.toString('hex', 0, 32), key.readUInt32BE(32, true)]; + assert(key.length === 37); + return [key.toString('hex', 1, 33), key.readUInt32BE(33, true)]; }, ih: function ih(ch, index, hash) { assert(typeof index === 'number'); @@ -240,9 +232,8 @@ layouts.txdb = { }, ihh: function ihh(key) { assert(Buffer.isBuffer(key)); - assert(key.length - 5 === 37); - key = key.slice(6); - return [key.readUInt32BE(0, true), key.toString('hex', 4, 36)]; + assert(key.length === 37); + return [key.readUInt32BE(1, true), key.toString('hex', 5, 37)]; }, iih: function iih(ch, index, num, hash) { assert(typeof index === 'number'); @@ -257,12 +248,11 @@ layouts.txdb = { }, iihh: function iihh(key) { assert(Buffer.isBuffer(key)); - assert(key.length - 5 === 41); - key = key.slice(6); + assert(key.length === 41); return [ - key.readUInt32BE(0, true), - key.readUInt32BE(4, true), - key.toString('hex', 8, 40) + key.readUInt32BE(1, true), + key.readUInt32BE(5, true), + key.toString('hex', 9, 41) ]; }, ihi: function ihi(ch, index, hash, num) { @@ -278,12 +268,11 @@ layouts.txdb = { }, ihii: function ihii(key) { assert(Buffer.isBuffer(key)); - assert(key.length - 5 === 41); - key = key.slice(6); + assert(key.length === 41); return [ - key.readUInt32BE(0, true), - key.toString('hex', 4, 36), - key.readUInt32BE(36, true) + key.readUInt32BE(1, true), + key.toString('hex', 5, 37), + key.readUInt32BE(37, true) ]; }, ha: function ha(ch, hash) { @@ -295,9 +284,8 @@ layouts.txdb = { }, haa: function haa(key) { assert(Buffer.isBuffer(key)); - assert(key.length - 5 === 33); - key = key.slice(6); - return key.toString('hex', 0); + assert(key.length === 33); + return key.toString('hex', 1, 33); }, t: function t(hash) { return this.ha(0x74, hash); @@ -371,9 +359,6 @@ layouts.txdb = { Cc: function Cc(key) { return this.ihii(key); }, - r: function r(hash) { - return this.ha(0x72, hash); - }, b: function b(height) { assert(typeof height === 'number'); const key = Buffer.allocUnsafe(5); @@ -383,8 +368,7 @@ layouts.txdb = { }, bb: function bb(key) { assert(Buffer.isBuffer(key)); - assert(key.length - 5 === 5); - key = key.slice(6); - return key.readUInt32BE(0, true); + assert(key.length === 5); + return key.readUInt32BE(1, true); } }; diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js index bfb67df2..b4ad789e 100644 --- a/lib/wallet/rpc.js +++ b/lib/wallet/rpc.js @@ -316,7 +316,7 @@ RPC.prototype.encryptWallet = async function encryptWallet(args, help) { throw new RPCError(errs.MISC_ERROR, 'encryptwallet "passphrase"'); try { - await wallet.setPassphrase(passphrase); + await wallet.encrypt(passphrase); } catch (e) { throw new RPCError(errs.WALLET_ENCRYPTION_FAILED, 'Encryption failed.'); } @@ -1432,15 +1432,15 @@ RPC.prototype.walletPassphraseChange = async function walletPassphraseChange(arg const valid = new Validator([args]); const old = valid.str(0, ''); - const new_ = valid.str(1, ''); + const passphrase = valid.str(1, ''); if (!wallet.master.encrypted) throw new RPCError(errs.WALLET_WRONG_ENC_STATE, 'Wallet is not encrypted.'); - if (old.length < 1 || new_.length < 1) + if (old.length < 1 || passphrase.length < 1) throw new RPCError(errs.INVALID_PARAMETER, 'Invalid parameter'); - await wallet.setPassphrase(old, new_); + await wallet.setPassphrase(passphrase, old); return null; }; diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index f6efd1e3..f580da9d 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -29,16 +29,20 @@ const TXRecord = records.TXRecord; * @param {Wallet} wallet */ -function TXDB(wallet) { +function TXDB(wdb) { if (!(this instanceof TXDB)) - return new TXDB(wallet); + return new TXDB(wdb); - this.wallet = wallet; - this.wdb = wallet.wdb; - this.db = this.wdb.db; - this.logger = this.wdb.logger; - this.network = this.wdb.network; - this.options = this.wdb.options; + this.wdb = wdb; + this.db = wdb.db; + this.logger = wdb.logger; + this.network = wdb.network; + this.options = wdb.options; + + this.wid = 0; + this.id = null; + this.prefix = layout.prefix(0); + this.wallet = null; this.locked = new Set(); this.state = null; @@ -58,15 +62,22 @@ TXDB.layout = layout; * @returns {Promise} */ -TXDB.prototype.open = async function open() { +TXDB.prototype.open = async function open(wallet) { + const {wid, id} = wallet; + + this.id = id; + this.wid = wid; + this.prefix = layout.prefix(wid); + this.wallet = wallet; + const state = await this.getState(); if (state) { this.state = state; - this.logger.info('TXDB loaded for %s.', this.wallet.id); + this.logger.info('TXDB loaded for %s.', id); } else { - this.state = new TXDBState(this.wallet.wid, this.wallet.id); - this.logger.info('TXDB created for %s.', this.wallet.id); + this.state = new TXDBState(wid, id); + this.logger.info('TXDB created for %s.', id); } this.logger.info('TXDB State: tx=%d coin=%s.', @@ -78,69 +89,6 @@ TXDB.prototype.open = async function open() { Amount.btc(this.state.confirmed)); }; -/** - * Start batch. - * @private - */ - -TXDB.prototype.start = function start() { - this.pending = this.state.clone(); - return this.wallet.start(); -}; - -/** - * Drop batch. - * @private - */ - -TXDB.prototype.drop = function drop() { - this.pending = null; - this.events.length = 0; - return this.wallet.drop(); -}; - -/** - * Clear batch. - * @private - */ - -TXDB.prototype.clear = function clear() { - this.pending = this.state.clone(); - this.events.length = 0; - return this.wallet.clear(); -}; - -/** - * Save batch. - * @returns {Promise} - */ - -TXDB.prototype.commit = async function commit() { - try { - await this.wallet.commit(); - } catch (e) { - this.pending = null; - this.events.length = 0; - throw e; - } - - // Overwrite the entire state - // with our new committed state. - if (this.pending.committed) { - this.state = this.pending; - - // Emit buffered events now that - // we know everything is written. - for (const [event, data, details] of this.events) { - this.wdb.emit(event, this.wallet.id, data, details); - this.wallet.emit(event, data, details); - } - } - - this.pending = null; - this.events.length = 0; -}; - /** * Emit transaction event. * @private @@ -150,39 +98,17 @@ TXDB.prototype.commit = async function commit() { */ TXDB.prototype.emit = function emit(event, data, details) { - this.events.push([event, data, details]); + this.wdb.emit(event, this.wallet, data, details); + this.wallet.emit(event, data, details); }; /** - * Prefix a key. - * @param {Buffer} key - * @returns {Buffer} Prefixed key. + * Bucket + * @returns {Bucket} */ -TXDB.prototype.prefix = function prefix(key) { - assert(this.wallet.wid); - return layout.prefix(this.wallet.wid, key); -}; - -/** - * Put key and value to current batch. - * @param {String} key - * @param {Buffer} value - */ - -TXDB.prototype.put = function put(key, value) { - assert(this.wallet.current); - this.wallet.current.put(this.prefix(key), value); -}; - -/** - * Delete key from current batch. - * @param {String} key - */ - -TXDB.prototype.del = function del(key) { - assert(this.wallet.current); - this.wallet.current.del(this.prefix(key)); +TXDB.prototype.bucket = function bucket() { + return this.db.bucket(this.prefix); }; /** @@ -191,7 +117,7 @@ TXDB.prototype.del = function del(key) { */ TXDB.prototype.get = function get(key) { - return this.db.get(this.prefix(key)); + return this.bucket().get(key); }; /** @@ -200,7 +126,7 @@ TXDB.prototype.get = function get(key) { */ TXDB.prototype.has = function has(key) { - return this.db.has(this.prefix(key)); + return this.bucket().has(key); }; /** @@ -210,13 +136,7 @@ TXDB.prototype.has = function has(key) { */ 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.range(options); + return this.bucket().range(options); }; /** @@ -226,13 +146,7 @@ TXDB.prototype.range = function range(options) { */ 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); + return this.bucket().keys(options); }; /** @@ -242,13 +156,7 @@ TXDB.prototype.keys = function keys(options) { */ 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); + return this.bucket().values(options); }; /** @@ -257,13 +165,13 @@ TXDB.prototype.values = function values(options) { * @returns {Promise} - Returns {@link Path}. */ -TXDB.prototype.getPath = async function getPath(output) { - const addr = output.getAddress(); +TXDB.prototype.getPath = function getPath(output) { + const hash = output.getHash('hex'); - if (!addr) + if (!hash) return null; - return await this.wallet.getPath(addr); + return this.wdb.getPath(this.wid, this.id, hash); }; /** @@ -272,13 +180,13 @@ TXDB.prototype.getPath = async function getPath(output) { * @returns {Promise} - Returns Boolean. */ -TXDB.prototype.hasPath = async function hasPath(output) { - const addr = output.getAddress(); +TXDB.prototype.hasPath = function hasPath(output) { + const hash = output.getHash('hex'); - if (!addr) + if (!hash) return false; - return await this.wallet.hasPath(addr); + return this.wdb.hasPath(this.wid, hash); }; /** @@ -287,14 +195,14 @@ TXDB.prototype.hasPath = async function hasPath(output) { * @param {Path} path */ -TXDB.prototype.saveCredit = async function saveCredit(credit, path) { +TXDB.prototype.saveCredit = async function saveCredit(b, credit, path) { const coin = credit.coin; const raw = credit.toRaw(); - await this.addOutpointMap(coin.hash, coin.index); + await this.addOutpointMap(b, coin.hash, coin.index); - this.put(layout.c(coin.hash, coin.index), raw); - this.put(layout.C(path.account, coin.hash, coin.index), null); + b.put(layout.c(coin.hash, coin.index), raw); + b.put(layout.C(path.account, coin.hash, coin.index), null); }; /** @@ -303,13 +211,13 @@ TXDB.prototype.saveCredit = async function saveCredit(credit, path) { * @param {Path} path */ -TXDB.prototype.removeCredit = async function removeCredit(credit, path) { +TXDB.prototype.removeCredit = async function removeCredit(b, credit, path) { const coin = credit.coin; - await this.removeOutpointMap(coin.hash, coin.index); + await this.removeOutpointMap(b, coin.hash, coin.index); - this.del(layout.c(coin.hash, coin.index)); - this.del(layout.C(path.account, coin.hash, coin.index)); + b.del(layout.c(coin.hash, coin.index)); + b.del(layout.C(path.account, coin.hash, coin.index)); }; /** @@ -319,11 +227,11 @@ TXDB.prototype.removeCredit = async function removeCredit(credit, path) { * @param {Number} index */ -TXDB.prototype.spendCredit = function spendCredit(credit, tx, index) { +TXDB.prototype.spendCredit = function spendCredit(b, credit, tx, index) { const prevout = tx.inputs[index].prevout; const spender = Outpoint.fromTX(tx, index); - this.put(layout.s(prevout.hash, prevout.index), spender.toRaw()); - this.put(layout.d(spender.hash, spender.index), credit.coin.toRaw()); + b.put(layout.s(prevout.hash, prevout.index), spender.toRaw()); + b.put(layout.d(spender.hash, spender.index), credit.coin.toRaw()); }; /** @@ -332,11 +240,11 @@ TXDB.prototype.spendCredit = function spendCredit(credit, tx, index) { * @param {Number} index */ -TXDB.prototype.unspendCredit = function unspendCredit(tx, index) { +TXDB.prototype.unspendCredit = function unspendCredit(b, tx, index) { const prevout = tx.inputs[index].prevout; const spender = Outpoint.fromTX(tx, index); - this.del(layout.s(prevout.hash, prevout.index)); - this.del(layout.d(spender.hash, spender.index)); + b.del(layout.s(prevout.hash, prevout.index)); + b.del(layout.d(spender.hash, spender.index)); }; /** @@ -345,10 +253,10 @@ TXDB.prototype.unspendCredit = function unspendCredit(tx, index) { * @param {Number} index */ -TXDB.prototype.writeInput = function writeInput(tx, index) { +TXDB.prototype.writeInput = function writeInput(b, tx, index) { const prevout = tx.inputs[index].prevout; const spender = Outpoint.fromTX(tx, index); - this.put(layout.s(prevout.hash, prevout.index), spender.toRaw()); + b.put(layout.s(prevout.hash, prevout.index), spender.toRaw()); }; /** @@ -357,9 +265,9 @@ TXDB.prototype.writeInput = function writeInput(tx, index) { * @param {Number} index */ -TXDB.prototype.removeInput = function removeInput(tx, index) { +TXDB.prototype.removeInput = function removeInput(b, tx, index) { const prevout = tx.inputs[index].prevout; - this.del(layout.s(prevout.hash, prevout.index)); + b.del(layout.s(prevout.hash, prevout.index)); }; /** @@ -371,7 +279,7 @@ TXDB.prototype.removeInput = function removeInput(tx, index) { * @returns {Boolean} */ -TXDB.prototype.resolveInput = async function resolveInput(tx, index, height, path, own) { +TXDB.prototype.resolveInput = async function resolveInput(b, state, tx, index, height, path, own) { const hash = tx.hash('hex'); const spent = await this.getSpent(hash, index); @@ -392,7 +300,7 @@ TXDB.prototype.resolveInput = async function resolveInput(tx, index, height, pat const credit = Credit.fromTX(tx, index, height); credit.own = own; - this.spendCredit(credit, stx.tx, spent.index); + this.spendCredit(b, credit, stx.tx, spent.index); // If the spender is unconfirmed, save // the credit as well, and mark it as @@ -402,9 +310,9 @@ TXDB.prototype.resolveInput = async function resolveInput(tx, index, height, pat // it retroactively. if (stx.height === -1) { credit.spent = true; - await this.saveCredit(credit, path); + await this.saveCredit(b, credit, path); if (height !== -1) - this.pending.confirmed += credit.coin.value; + state.confirmed += credit.coin.value; } return true; @@ -427,26 +335,6 @@ TXDB.prototype.isDoubleSpend = async function isDoubleSpend(tx) { return false; }; -/** - * Test an entire transaction to see - * if any of its outpoints are replace by fee. - * @param {TX} tx - * @returns {Promise} - Returns Boolean. - */ - -TXDB.prototype.isRBF = async function isRBF(tx) { - if (tx.isRBF()) - return true; - - for (const {prevout} of tx.inputs) { - const key = layout.r(prevout.hash); - if (await this.has(key)) - return true; - } - - return false; -}; - /** * Test a whether a coin has been spent. * @param {Hash} hash @@ -480,8 +368,8 @@ TXDB.prototype.isSpent = function isSpent(hash, index) { * @returns {Promise} */ -TXDB.prototype.addBlockMap = function addBlockMap(height) { - return this.wdb.addBlockMap(this.wallet, height); +TXDB.prototype.addBlockMap = function addBlockMap(b, height) { + return this.wdb.addBlockMap(b.batch, height, this.wid); }; /** @@ -490,8 +378,8 @@ TXDB.prototype.addBlockMap = function addBlockMap(height) { * @returns {Promise} */ -TXDB.prototype.removeBlockMap = function removeBlockMap(height) { - return this.wdb.removeBlockMap(this.wallet, height); +TXDB.prototype.removeBlockMap = function removeBlockMap(b, height) { + return this.wdb.removeBlockMap(b.batch, height, this.wid); }; /** @@ -500,8 +388,8 @@ TXDB.prototype.removeBlockMap = function removeBlockMap(height) { * @returns {Promise} */ -TXDB.prototype.addTXMap = function addTXMap(hash) { - return this.wdb.addTXMap(this.wallet, hash); +TXDB.prototype.addTXMap = function addTXMap(b, hash) { + return this.wdb.addTXMap(b.batch, hash, this.wid); }; /** @@ -510,8 +398,8 @@ TXDB.prototype.addTXMap = function addTXMap(hash) { * @returns {Promise} */ -TXDB.prototype.removeTXMap = function removeTXMap(hash) { - return this.wdb.removeTXMap(this.wallet, hash); +TXDB.prototype.removeTXMap = function removeTXMap(b, hash) { + return this.wdb.removeTXMap(b.batch, hash, this.wid); }; /** @@ -521,8 +409,8 @@ TXDB.prototype.removeTXMap = function removeTXMap(hash) { * @returns {Promise} */ -TXDB.prototype.addOutpointMap = function addOutpointMap(hash, index) { - return this.wdb.addOutpointMap(this.wallet, hash, index); +TXDB.prototype.addOutpointMap = function addOutpointMap(b, hash, index) { + return this.wdb.addOutpointMap(b.batch, hash, index, this.wid); }; /** @@ -532,8 +420,8 @@ TXDB.prototype.addOutpointMap = function addOutpointMap(hash, index) { * @returns {Promise} */ -TXDB.prototype.removeOutpointMap = function removeOutpointMap(hash, index) { - return this.wdb.removeOutpointMap(this.wallet, hash, index); +TXDB.prototype.removeOutpointMap = function removeOutpointMap(b, hash, index) { + return this.wdb.removeOutpointMap(b.batch, hash, index, this.wid); }; /** @@ -571,8 +459,9 @@ TXDB.prototype.getBlock = async function getBlock(height) { * @returns {Promise} */ -TXDB.prototype.addBlock = async function addBlock(hash, meta) { +TXDB.prototype.addBlock = async function addBlock(b, hash, meta) { const key = layout.b(meta.height); + let data = await this.get(key); let block; @@ -588,7 +477,7 @@ TXDB.prototype.addBlock = async function addBlock(hash, meta) { block.writeUInt32LE(size + 1, 40, true); hash.copy(block, data.length); - this.put(key, block); + b.put(key, block); }; /** @@ -598,7 +487,7 @@ TXDB.prototype.addBlock = async function addBlock(hash, meta) { * @returns {Promise} */ -TXDB.prototype.removeBlock = async function removeBlock(hash, height) { +TXDB.prototype.removeBlock = async function removeBlock(b, hash, height) { const key = layout.b(height); const data = await this.get(key); @@ -611,14 +500,14 @@ TXDB.prototype.removeBlock = async function removeBlock(hash, height) { assert(data.slice(-32).equals(hash)); if (size === 1) { - this.del(key); + b.del(key); return; } const block = data.slice(0, -32); block.writeUInt32LE(size - 1, 40, true); - this.put(key, block); + b.put(key, block); }; /** @@ -628,7 +517,7 @@ TXDB.prototype.removeBlock = async function removeBlock(hash, height) { * @returns {Promise} */ -TXDB.prototype.addBlockSlow = async function addBlockSlow(hash, meta) { +TXDB.prototype.addBlockSlow = async function addBlockSlow(b, hash, meta) { let block = await this.getBlock(meta.height); if (!block) @@ -637,7 +526,7 @@ TXDB.prototype.addBlockSlow = async function addBlockSlow(hash, meta) { if (!block.add(hash)) return; - this.put(layout.b(meta.height), block.toRaw()); + b.put(layout.b(meta.height), block.toRaw()); }; /** @@ -647,7 +536,7 @@ TXDB.prototype.addBlockSlow = async function addBlockSlow(hash, meta) { * @returns {Promise} */ -TXDB.prototype.removeBlockSlow = async function removeBlockSlow(hash, height) { +TXDB.prototype.removeBlockSlow = async function removeBlockSlow(b, hash, height) { const block = await this.getBlock(height); if (!block) @@ -657,35 +546,11 @@ TXDB.prototype.removeBlockSlow = async function removeBlockSlow(hash, height) { return; if (block.hashes.size === 0) { - this.del(layout.b(height)); + b.del(layout.b(height)); return; } - this.put(layout.b(height), block.toRaw()); -}; - -/** - * Add transaction, potentially runs - * `confirm()` and `removeConflicts()`. - * @param {TX} tx - * @param {BlockMeta} block - * @returns {Promise} - */ - -TXDB.prototype.add = async function add(tx, block) { - this.start(); - - let result; - try { - result = await this._add(tx, block); - } catch (e) { - this.drop(); - throw e; - } - - await this.commit(); - - return result; + b.put(layout.b(height), block.toRaw()); }; /** @@ -695,7 +560,7 @@ TXDB.prototype.add = async function add(tx, block) { * @returns {Promise} */ -TXDB.prototype._add = async function _add(tx, block) { +TXDB.prototype.add = async function add(tx, block) { const hash = tx.hash('hex'); const existing = await this.getTX(hash); @@ -718,16 +583,6 @@ TXDB.prototype._add = async function _add(tx, block) { const wtx = TXRecord.fromTX(tx, block); if (!block) { - // We ignore any unconfirmed txs - // that are replace-by-fee. - if (await this.isRBF(tx)) { - // We need to index every spender - // hash to detect "passive" - // replace-by-fee. - this.put(layout.r(hash), null); - return null; - } - // Potentially remove double-spenders. // Only remove if they're not confirmed. if (!await this.removeConflicts(tx, true)) @@ -735,9 +590,6 @@ TXDB.prototype._add = async function _add(tx, block) { } else { // Potentially remove double-spenders. await this.removeConflicts(tx, false); - - // Delete the replace-by-fee record. - this.del(layout.r(hash)); } // Finally we can do a regular insertion. @@ -753,6 +605,8 @@ TXDB.prototype._add = async function _add(tx, block) { */ TXDB.prototype.insert = async function insert(wtx, block) { + const b = this.bucket(); + const state = this.state.clone(); const tx = wtx.tx; const hash = wtx.hash; const height = block ? block.height : -1; @@ -781,7 +635,7 @@ TXDB.prototype.insert = async function insert(wtx, block) { // necessary assuming no messages // are ever missed from the mempool, // but shit happens. - this.writeInput(tx, i); + this.writeInput(b, tx, i); continue; } @@ -805,14 +659,14 @@ TXDB.prototype.insert = async function insert(wtx, block) { // Write an undo coin for the credit // and add it to the stxo set. - this.spendCredit(credit, tx, i); + this.spendCredit(b, credit, tx, i); // Unconfirmed balance should always // be updated as it reflects the on-chain // balance _and_ mempool balance assuming // everything in the mempool were to confirm. - this.pending.coin--; - this.pending.unconfirmed -= coin.value; + state.coin -= 1; + state.unconfirmed -= coin.value; if (!block) { // If the tx is not mined, we do not @@ -824,15 +678,15 @@ TXDB.prototype.insert = async function insert(wtx, block) { // possible to compare the on-chain // state vs. the mempool state. credit.spent = true; - await this.saveCredit(credit, path); + await this.saveCredit(b, credit, path); } else { // If the tx is mined, we can safely // remove the coin being spent. This // coin will be indexed as an undo // coin so it can be reconnected // later during a reorg. - this.pending.confirmed -= coin.value; - await this.removeCredit(credit, path); + state.confirmed -= coin.value; + await this.removeCredit(b, credit, path); } updated = true; @@ -853,7 +707,7 @@ TXDB.prototype.insert = async function insert(wtx, block) { // Attempt to resolve an input we // did not know was ours at the time. - if (await this.resolveInput(tx, i, height, path, own)) { + if (await this.resolveInput(b, state, tx, i, height, path, own)) { updated = true; continue; } @@ -861,13 +715,13 @@ TXDB.prototype.insert = async function insert(wtx, block) { const credit = Credit.fromTX(tx, i, height); credit.own = own; - this.pending.coin++; - this.pending.unconfirmed += output.value; + state.coin += 1; + state.unconfirmed += output.value; if (block) - this.pending.confirmed += output.value; + state.confirmed += output.value; - await this.saveCredit(credit, path); + await this.saveCredit(b, credit, path); updated = true; } @@ -881,33 +735,33 @@ TXDB.prototype.insert = async function insert(wtx, block) { } // Save and index the transaction record. - this.put(layout.t(hash), wtx.toRaw()); - this.put(layout.m(wtx.mtime, hash), null); + b.put(layout.t(hash), wtx.toRaw()); + b.put(layout.m(wtx.mtime, hash), null); if (!block) - this.put(layout.p(hash), null); + b.put(layout.p(hash), null); else - this.put(layout.h(height, hash), null); + b.put(layout.h(height, hash), null); // Do some secondary indexing for account-based // queries. This saves us a lot of time for // queries later. for (const account of accounts) { - this.put(layout.T(account, hash), null); - this.put(layout.M(account, wtx.mtime, hash), null); + b.put(layout.T(account, hash), null); + b.put(layout.M(account, wtx.mtime, hash), null); if (!block) - this.put(layout.P(account, hash), null); + b.put(layout.P(account, hash), null); else - this.put(layout.H(account, height, hash), null); + b.put(layout.H(account, height, hash), null); } - await this.addTXMap(hash); + await this.addTXMap(b, hash); // Update block records. if (block) { - await this.addBlockMap(height); - await this.addBlock(tx.hash(), block); + await this.addBlockMap(b, height); + await this.addBlock(b, tx.hash(), block); } // Update the transaction counter and @@ -915,19 +769,23 @@ TXDB.prototype.insert = async function insert(wtx, block) { // only overwrite the best state once // the batch has actually been written // to disk. - this.pending.tx++; - this.put(layout.R, this.pending.commit()); + state.tx += 1; + b.put(layout.R, state.commit()); // This transaction may unlock some // coins now that we've seen it. this.unlockTX(tx); + await b.write(); + + this.state = state; + // Emit events for potential local and // websocket listeners. Note that these // will only be emitted if the batch is // successfully written to disk. this.emit('tx', tx, details); - this.emit('balance', this.pending.toBalance(), details); + this.emit('balance', state.toBalance(), details); return details; }; @@ -951,20 +809,7 @@ TXDB.prototype.confirm = async function confirm(hash, block) { assert(block); - this.start(); - - let details; - - try { - details = await this._confirm(wtx, block); - } catch (e) { - this.drop(); - throw e; - } - - await this.commit(); - - return details; + return this._confirm(wtx, block); }; /** @@ -976,6 +821,8 @@ TXDB.prototype.confirm = async function confirm(hash, block) { */ TXDB.prototype._confirm = async function _confirm(wtx, block) { + const b = this.batch(); + const state = this.state.clone(); const tx = wtx.tx; const hash = wtx.hash; const height = block.height; @@ -1009,10 +856,10 @@ TXDB.prototype._confirm = async function _confirm(wtx, block) { // We don't need to remove the coin // since it was never added in the // first place. - this.spendCredit(credit, tx, i); + this.spendCredit(b, credit, tx, i); - this.pending.coin--; - this.pending.unconfirmed -= credit.coin.value; + state.coin -= 1; + state.unconfirmed -= credit.coin.value; } const coin = credit.coin; @@ -1028,9 +875,9 @@ TXDB.prototype._confirm = async function _confirm(wtx, block) { // We can now safely remove the credit // entirely, now that we know it's also // been removed on-chain. - this.pending.confirmed -= coin.value; + state.confirmed -= coin.value; - await this.removeCredit(credit, path); + await this.removeCredit(b, credit, path); } } @@ -1053,44 +900,45 @@ TXDB.prototype._confirm = async function _confirm(wtx, block) { // spent in the mempool, we need to // update the undo coin's height. if (credit.spent) - await this.updateSpentCoin(tx, i, height); + await this.updateSpentCoin(b, tx, i, height); // Update coin height and confirmed // balance. Save once again. const coin = credit.coin; coin.height = height; - this.pending.confirmed += output.value; + state.confirmed += output.value; - await this.saveCredit(credit, path); + await this.saveCredit(b, credit, path); } - // Remove the RBF index if we have one. - this.del(layout.r(hash)); - // Save the new serialized transaction as // the block-related properties have been // updated. Also reindex for height. - this.put(layout.t(hash), wtx.toRaw()); - this.del(layout.p(hash)); - this.put(layout.h(height, hash), null); + b.put(layout.t(hash), wtx.toRaw()); + b.del(layout.p(hash)); + b.put(layout.h(height, hash), null); // Secondary indexing also needs to change. for (const account of accounts) { - this.del(layout.P(account, hash)); - this.put(layout.H(account, height, hash), null); + b.del(layout.P(account, hash)); + b.put(layout.H(account, height, hash), null); } - await this.addBlockMap(height); - await this.addBlock(tx.hash(), block); + await this.addBlockMap(b, height); + await this.addBlock(b, tx.hash(), block); // Commit the new state. The balance has updated. - this.put(layout.R, this.pending.commit()); + b.put(layout.R, state.commit()); + + await b.write(); + + this.state = state; this.unlockTX(tx); this.emit('confirmed', tx, details); - this.emit('balance', this.pending.toBalance(), details); + this.emit('balance', state.toBalance(), details); return details; }; @@ -1120,6 +968,8 @@ TXDB.prototype.remove = async function remove(hash) { */ TXDB.prototype.erase = async function erase(wtx, block) { + const b = this.bucket(); + const state = this.state.clone(); const tx = wtx.tx; const hash = wtx.hash; const height = block ? block.height : -1; @@ -1139,7 +989,7 @@ TXDB.prototype.erase = async function erase(wtx, block) { // This input never had an undo // coin, but remove it from the // stxo set. - this.removeInput(tx, i); + this.removeInput(b, tx, i); continue; } @@ -1153,14 +1003,14 @@ TXDB.prototype.erase = async function erase(wtx, block) { // Recalculate the balance, remove // from stxo set, remove the undo // coin, and resave the credit. - this.pending.coin++; - this.pending.unconfirmed += coin.value; + state.coin += 1; + state.unconfirmed += coin.value; if (block) - this.pending.confirmed += coin.value; + state.confirmed += coin.value; - this.unspendCredit(tx, i); - await this.saveCredit(credit, path); + this.unspendCredit(b, tx, i); + await this.saveCredit(b, credit, path); } } @@ -1178,55 +1028,56 @@ TXDB.prototype.erase = async function erase(wtx, block) { const credit = Credit.fromTX(tx, i, height); - this.pending.coin--; - this.pending.unconfirmed -= output.value; + state.coin -= 1; + state.unconfirmed -= output.value; if (block) - this.pending.confirmed -= output.value; + state.confirmed -= output.value; - await this.removeCredit(credit, path); + await this.removeCredit(b, credit, path); } - // Remove the RBF index if we have one. - this.del(layout.r(hash)); - // Remove the transaction data // itself as well as unindex. - this.del(layout.t(hash)); - this.del(layout.m(wtx.mtime, hash)); + b.del(layout.t(hash)); + b.del(layout.m(wtx.mtime, hash)); if (!block) - this.del(layout.p(hash)); + b.del(layout.p(hash)); else - this.del(layout.h(height, hash)); + b.del(layout.h(height, hash)); // Remove all secondary indexing. for (const account of accounts) { - this.del(layout.T(account, hash)); - this.del(layout.M(account, wtx.mtime, hash)); + b.del(layout.T(account, hash)); + b.del(layout.M(account, wtx.mtime, hash)); if (!block) - this.del(layout.P(account, hash)); + b.del(layout.P(account, hash)); else - this.del(layout.H(account, height, hash)); + b.del(layout.H(account, height, hash)); } - await this.removeTXMap(hash); + await this.removeTXMap(b, hash); // Update block records. if (block) { - await this.removeBlockMap(height); - await this.removeBlockSlow(hash, height); + await this.removeBlockMap(b, height); + await this.removeBlockSlow(b, hash, height); } // Update the transaction counter // and commit new state due to // balance change. - this.pending.tx--; - this.put(layout.R, this.pending.commit()); + state.tx -= 1; + b.put(layout.R, state.commit()); + + await b.write(); + + this.state = state; this.emit('remove tx', tx, details); - this.emit('balance', this.pending.toBalance(), details); + this.emit('balance', state.toBalance(), details); return details; }; @@ -1257,15 +1108,11 @@ TXDB.prototype.removeRecursive = async function removeRecursive(wtx) { await this.removeRecursive(stx); } - this.start(); - // Remove the spender. const details = await this.erase(wtx, wtx.getBlock()); assert(details); - await this.commit(); - return details; }; @@ -1291,28 +1138,6 @@ TXDB.prototype.revert = async function revert(height) { return block.hashes.length; }; -/** - * Unconfirm a transaction. Necessary after a reorg. - * @param {Hash} hash - * @returns {Promise} - */ - -TXDB.prototype.unconfirm = async function unconfirm(hash) { - this.start(); - - let details; - try { - details = await this._unconfirm(hash); - } catch (e) { - this.drop(); - throw e; - } - - await this.commit(); - - return details; -}; - /** * Unconfirm a transaction without a batch. * @private @@ -1320,7 +1145,7 @@ TXDB.prototype.unconfirm = async function unconfirm(hash) { * @returns {Promise} */ -TXDB.prototype._unconfirm = async function _unconfirm(hash) { +TXDB.prototype.unconfirm = async function unconfirm(hash) { const wtx = await this.getTX(hash); if (!wtx) @@ -1339,6 +1164,8 @@ TXDB.prototype._unconfirm = async function _unconfirm(hash) { */ TXDB.prototype.disconnect = async function disconnect(wtx, block) { + const b = this.bucket(); + const state = this.state.clone(); const tx = wtx.tx; const hash = wtx.hash; const height = block.height; @@ -1371,12 +1198,12 @@ TXDB.prototype.disconnect = async function disconnect(wtx, block) { details.setInput(i, path, coin); accounts.add(path.account); - this.pending.confirmed += coin.value; + state.confirmed += coin.value; // Resave the credit and mark it // as spent in the mempool instead. credit.spent = true; - await this.saveCredit(credit, path); + await this.saveCredit(b, credit, path); } } @@ -1393,12 +1220,12 @@ TXDB.prototype.disconnect = async function disconnect(wtx, block) { // Potentially update undo coin height. if (!credit) { - await this.updateSpentCoin(tx, i, height); + await this.updateSpentCoin(b, tx, i, height); continue; } if (credit.spent) - await this.updateSpentCoin(tx, i, height); + await this.updateSpentCoin(b, tx, i, height); details.setOutput(i, path); accounts.add(path.account); @@ -1408,30 +1235,34 @@ TXDB.prototype.disconnect = async function disconnect(wtx, block) { const coin = credit.coin; coin.height = -1; - this.pending.confirmed -= output.value; + state.confirmed -= output.value; - await this.saveCredit(credit, path); + await this.saveCredit(b, credit, path); } - await this.removeBlockMap(height); - await this.removeBlock(tx.hash(), height); + await this.removeBlockMap(b, height); + await this.removeBlock(b, tx.hash(), height); // We need to update the now-removed // block properties and reindex due // to the height change. - this.put(layout.t(hash), wtx.toRaw()); - this.put(layout.p(hash), null); - this.del(layout.h(height, hash)); + b.put(layout.t(hash), wtx.toRaw()); + b.put(layout.p(hash), null); + b.del(layout.h(height, hash)); // Secondary indexing also needs to change. for (const account of accounts) { - this.put(layout.P(account, hash), null); - this.del(layout.H(account, height, hash)); + b.put(layout.P(account, hash), null); + b.del(layout.H(account, height, hash)); } // Commit state due to unconfirmed // vs. confirmed balance change. - this.put(layout.R, this.pending.commit()); + b.put(layout.R, state.commit()); + + await b.write(); + + this.state = state; this.emit('unconfirmed', tx, details); this.emit('balance', this.pending.toBalance(), details); @@ -1456,12 +1287,8 @@ TXDB.prototype.removeConflict = async function removeConflict(wtx) { this.logger.warning('Handling conflicting tx: %s.', tx.txid()); - this.drop(); - const details = await this.removeRecursive(wtx); - this.start(); - this.logger.warning('Removed conflict: %s.', tx.txid()); // Emit the _removed_ transaction. @@ -1633,14 +1460,14 @@ TXDB.prototype.getLocked = function getLocked() { /** * Get hashes of all transactions in the database. - * @param {Number?} account + * @param {Number} acct * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getAccountHistoryHashes = function getAccountHistoryHashes(account) { +TXDB.prototype.getAccountHistoryHashes = function getAccountHistoryHashes(acct) { return this.keys({ - gte: layout.T(account, encoding.NULL_HASH), - lte: layout.T(account, encoding.HIGH_HASH), + gte: layout.T(acct, encoding.NULL_HASH), + lte: layout.T(acct, encoding.HIGH_HASH), parse: (key) => { const [, hash] = layout.Tt(key); return hash; @@ -1650,13 +1477,15 @@ TXDB.prototype.getAccountHistoryHashes = function getAccountHistoryHashes(accoun /** * Get hashes of all transactions in the database. - * @param {Number?} account + * @param {Number} acct * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getHistoryHashes = function getHistoryHashes(account) { - if (account != null) - return this.getAccountHistoryHashes(account); +TXDB.prototype.getHistoryHashes = function getHistoryHashes(acct) { + assert(typeof acct === 'number'); + + if (acct !== -1) + return this.getAccountHistoryHashes(acct); return this.keys({ gte: layout.t(encoding.NULL_HASH), @@ -1667,14 +1496,14 @@ TXDB.prototype.getHistoryHashes = function getHistoryHashes(account) { /** * Get hashes of all unconfirmed transactions in the database. - * @param {Number?} account + * @param {Number} acct * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getAccountPendingHashes = function getAccountPendingHashes(account) { +TXDB.prototype.getAccountPendingHashes = function getAccountPendingHashes(acct) { return this.keys({ - gte: layout.P(account, encoding.NULL_HASH), - lte: layout.P(account, encoding.HIGH_HASH), + gte: layout.P(acct, encoding.NULL_HASH), + lte: layout.P(acct, encoding.HIGH_HASH), parse: (key) => { const [, hash] = layout.Pp(key); return hash; @@ -1684,13 +1513,15 @@ TXDB.prototype.getAccountPendingHashes = function getAccountPendingHashes(accoun /** * Get hashes of all unconfirmed transactions in the database. - * @param {Number?} account + * @param {Number} acct * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getPendingHashes = function getPendingHashes(account) { - if (account != null) - return this.getAccountPendingHashes(account); +TXDB.prototype.getPendingHashes = function getPendingHashes(acct) { + assert(typeof acct === 'number'); + + if (acct !== -1) + return this.getAccountPendingHashes(acct); return this.keys({ gte: layout.p(encoding.NULL_HASH), @@ -1701,14 +1532,14 @@ TXDB.prototype.getPendingHashes = function getPendingHashes(account) { /** * Get all coin hashes in the database. - * @param {Number?} account + * @param {Number} acct * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getAccountOutpoints = function getAccountOutpoints(account) { +TXDB.prototype.getAccountOutpoints = function getAccountOutpoints(acct) { return this.keys({ - gte: layout.C(account, encoding.NULL_HASH, 0), - lte: layout.C(account, encoding.HIGH_HASH, 0xffffffff), + gte: layout.C(acct, encoding.NULL_HASH, 0), + lte: layout.C(acct, encoding.HIGH_HASH, 0xffffffff), parse: (key) => { const [, hash, index] = layout.Cc(key); return new Outpoint(hash, index); @@ -1718,13 +1549,15 @@ TXDB.prototype.getAccountOutpoints = function getAccountOutpoints(account) { /** * Get all coin hashes in the database. - * @param {Number?} account + * @param {Number} acct * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getOutpoints = function getOutpoints(account) { - if (account != null) - return this.getAccountOutpoints(account); +TXDB.prototype.getOutpoints = function getOutpoints(acct) { + assert(typeof acct === 'number'); + + if (acct !== -1) + return this.getAccountOutpoints(acct); return this.keys({ gte: layout.c(encoding.NULL_HASH, 0), @@ -1738,7 +1571,7 @@ TXDB.prototype.getOutpoints = function getOutpoints(account) { /** * Get TX hashes by height range. - * @param {Number?} account + * @param {Number} acct * @param {Object} options * @param {Number} options.start - Start height. * @param {Number} options.end - End height. @@ -1747,13 +1580,13 @@ TXDB.prototype.getOutpoints = function getOutpoints(account) { * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getAccountHeightRangeHashes = function getAccountHeightRangeHashes(account, options) { +TXDB.prototype.getAccountHeightRangeHashes = function getAccountHeightRangeHashes(acct, options) { const start = options.start || 0; const end = options.end || 0xffffffff; return this.keys({ - gte: layout.H(account, start, encoding.NULL_HASH), - lte: layout.H(account, end, encoding.HIGH_HASH), + gte: layout.H(acct, start, encoding.NULL_HASH), + lte: layout.H(acct, end, encoding.HIGH_HASH), limit: options.limit, reverse: options.reverse, parse: (key) => { @@ -1765,7 +1598,7 @@ TXDB.prototype.getAccountHeightRangeHashes = function getAccountHeightRangeHashe /** * Get TX hashes by height range. - * @param {Number?} account + * @param {Number} acct * @param {Object} options * @param {Number} options.start - Start height. * @param {Number} options.end - End height. @@ -1774,14 +1607,11 @@ TXDB.prototype.getAccountHeightRangeHashes = function getAccountHeightRangeHashe * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(account, options) { - if (account && typeof account === 'object') { - options = account; - account = null; - } +TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(acct, options) { + assert(typeof acct === 'number'); - if (account != null) - return this.getAccountHeightRangeHashes(account, options); + if (acct !== -1) + return this.getAccountHeightRangeHashes(acct, options); const start = options.start || 0; const end = options.end || 0xffffffff; @@ -1810,7 +1640,7 @@ TXDB.prototype.getHeightHashes = function getHeightHashes(height) { /** * Get TX hashes by timestamp range. - * @param {Number?} account + * @param {Number} acct * @param {Object} options * @param {Number} options.start - Start height. * @param {Number} options.end - End height. @@ -1819,13 +1649,13 @@ TXDB.prototype.getHeightHashes = function getHeightHashes(height) { * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getAccountRangeHashes = function getAccountRangeHashes(account, options) { +TXDB.prototype.getAccountRangeHashes = function getAccountRangeHashes(acct, options) { const start = options.start || 0; const end = options.end || 0xffffffff; return this.keys({ - gte: layout.M(account, start, encoding.NULL_HASH), - lte: layout.M(account, end, encoding.HIGH_HASH), + gte: layout.M(acct, start, encoding.NULL_HASH), + lte: layout.M(acct, end, encoding.HIGH_HASH), limit: options.limit, reverse: options.reverse, parse: (key) => { @@ -1837,7 +1667,7 @@ TXDB.prototype.getAccountRangeHashes = function getAccountRangeHashes(account, o /** * Get TX hashes by timestamp range. - * @param {Number?} account + * @param {Number} acct * @param {Object} options * @param {Number} options.start - Start height. * @param {Number} options.end - End height. @@ -1846,14 +1676,11 @@ TXDB.prototype.getAccountRangeHashes = function getAccountRangeHashes(account, o * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getRangeHashes = function getRangeHashes(account, options) { - if (account && typeof account === 'object') { - options = account; - account = null; - } +TXDB.prototype.getRangeHashes = function getRangeHashes(acct, options) { + assert(typeof acct === 'number'); - if (account != null) - return this.getAccountRangeHashes(account, options); + if (acct !== -1) + return this.getAccountRangeHashes(acct, options); const start = options.start || 0; const end = options.end || 0xffffffff; @@ -1872,7 +1699,7 @@ TXDB.prototype.getRangeHashes = function getRangeHashes(account, options) { /** * Get transactions by timestamp range. - * @param {Number?} account + * @param {Number} acct * @param {Object} options * @param {Number} options.start - Start time. * @param {Number} options.end - End time. @@ -1881,16 +1708,10 @@ TXDB.prototype.getRangeHashes = function getRangeHashes(account, options) { * @returns {Promise} - Returns {@link TX}[]. */ -TXDB.prototype.getRange = async function getRange(account, options) { +TXDB.prototype.getRange = async function getRange(acct, options) { + const hashes = await this.getRangeHashes(acct, options); const txs = []; - if (account && typeof account === 'object') { - options = account; - account = null; - } - - const hashes = await this.getRangeHashes(account, options); - for (const hash of hashes) { const tx = await this.getTX(hash); assert(tx); @@ -1902,13 +1723,13 @@ TXDB.prototype.getRange = async function getRange(account, options) { /** * Get last N transactions. - * @param {Number?} account + * @param {Number} acct * @param {Number} limit - Max number of transactions. * @returns {Promise} - Returns {@link TX}[]. */ -TXDB.prototype.getLast = function getLast(account, limit) { - return this.getRange(account, { +TXDB.prototype.getLast = function getLast(acct, limit) { + return this.getRange(acct, { start: 0, end: 0xffffffff, reverse: true, @@ -1918,14 +1739,16 @@ TXDB.prototype.getLast = function getLast(account, limit) { /** * Get all transactions. - * @param {Number?} account + * @param {Number} acct * @returns {Promise} - Returns {@link TX}[]. */ -TXDB.prototype.getHistory = function getHistory(account) { +TXDB.prototype.getHistory = function getHistory(acct) { + assert(typeof acct === 'number'); + // Slow case - if (account != null) - return this.getAccountHistory(account); + if (acct !== -1) + return this.getAccountHistory(acct); // Fast case return this.values({ @@ -1936,13 +1759,13 @@ TXDB.prototype.getHistory = function getHistory(account) { }; /** - * Get all account transactions. - * @param {Number?} account + * Get all acct transactions. + * @param {Number} acct * @returns {Promise} - Returns {@link TX}[]. */ -TXDB.prototype.getAccountHistory = async function getAccountHistory(account) { - const hashes = await this.getHistoryHashes(account); +TXDB.prototype.getAccountHistory = async function getAccountHistory(acct) { + const hashes = await this.getHistoryHashes(acct); const txs = []; for (const hash of hashes) { @@ -1956,12 +1779,12 @@ TXDB.prototype.getAccountHistory = async function getAccountHistory(account) { /** * Get unconfirmed transactions. - * @param {Number?} account + * @param {Number} acct * @returns {Promise} - Returns {@link TX}[]. */ -TXDB.prototype.getPending = async function getPending(account) { - const hashes = await this.getPendingHashes(account); +TXDB.prototype.getPending = async function getPending(acct) { + const hashes = await this.getPendingHashes(acct); const txs = []; for (const hash of hashes) { @@ -1975,14 +1798,16 @@ TXDB.prototype.getPending = async function getPending(account) { /** * Get coins. - * @param {Number?} account + * @param {Number} acct * @returns {Promise} - Returns {@link Coin}[]. */ -TXDB.prototype.getCredits = function getCredits(account) { +TXDB.prototype.getCredits = function getCredits(acct) { + assert(typeof acct === 'number'); + // Slow case - if (account != null) - return this.getAccountCredits(account); + if (acct !== -1) + return this.getAccountCredits(acct); // Fast case return this.range({ @@ -2000,16 +1825,16 @@ TXDB.prototype.getCredits = function getCredits(account) { /** * Get coins by account. - * @param {Number} account + * @param {Number} acct * @returns {Promise} - Returns {@link Coin}[]. */ -TXDB.prototype.getAccountCredits = async function getAccountCredits(account) { - const outpoints = await this.getOutpoints(account); +TXDB.prototype.getAccountCredits = async function getAccountCredits(acct) { + const outpoints = await this.getOutpoints(acct); const credits = []; - for (const prevout of outpoints) { - const credit = await this.getCredit(prevout.hash, prevout.index); + for (const {hash, index} of outpoints) { + const credit = await this.getCredit(hash, index); assert(credit); credits.push(credit); } @@ -2052,12 +1877,12 @@ TXDB.prototype.getSpentCredits = async function getSpentCredits(tx) { /** * Get coins. - * @param {Number?} account + * @param {Number} acct * @returns {Promise} - Returns {@link Coin}[]. */ -TXDB.prototype.getCoins = async function getCoins(account) { - const credits = await this.getCredits(account); +TXDB.prototype.getCoins = async function getCoins(acct) { + const credits = await this.getCredits(acct); const coins = []; for (const credit of credits) { @@ -2072,12 +1897,12 @@ TXDB.prototype.getCoins = async function getCoins(account) { /** * Get coins by account. - * @param {Number} account + * @param {Number} acct * @returns {Promise} - Returns {@link Coin}[]. */ -TXDB.prototype.getAccountCoins = async function getAccountCoins(account) { - const credits = await this.getAccountCredits(account); +TXDB.prototype.getAccountCoins = async function getAccountCoins(acct) { + const credits = await this.getAccountCredits(acct); const coins = []; for (const credit of credits) { @@ -2175,7 +2000,7 @@ TXDB.prototype.getState = async function getState() { if (!data) return null; - return TXDBState.fromRaw(this.wallet.wid, this.wallet.id, data); + return TXDBState.fromRaw(this.wid, this.id, data); }; /** @@ -2381,10 +2206,12 @@ TXDB.prototype.hasCoin = async function hasCoin(hash, index) { * @returns {Promise} - Returns {@link Balance}. */ -TXDB.prototype.getBalance = async function getBalance(account) { +TXDB.prototype.getBalance = async function getBalance(acct) { + assert(typeof acct === 'number'); + // Slow case - if (account != null) - return await this.getAccountBalance(account); + if (acct !== -1) + return await this.getAccountBalance(acct); // Fast case return this.state.toBalance(); @@ -2392,13 +2219,12 @@ TXDB.prototype.getBalance = async function getBalance(account) { /** * Calculate balance. - * @param {Number?} account * @returns {Promise} - Returns {@link Balance}. */ TXDB.prototype.getWalletBalance = async function getWalletBalance() { const credits = await this.getCredits(); - const balance = new Balance(this.wallet.wid, this.wallet.id, -1); + const balance = new Balance(this.wid, this.id, -1); for (const credit of credits) { const coin = credit.coin; @@ -2415,13 +2241,13 @@ TXDB.prototype.getWalletBalance = async function getWalletBalance() { /** * Calculate balance by account. - * @param {Number} account + * @param {Number} acct * @returns {Promise} - Returns {@link Balance}. */ -TXDB.prototype.getAccountBalance = async function getAccountBalance(account) { - const credits = await this.getAccountCredits(account); - const balance = new Balance(this.wallet.wid, this.wallet.id, account); +TXDB.prototype.getAccountBalance = async function getAccountBalance(acct) { + const credits = await this.getAccountCredits(acct); + const balance = new Balance(this.wid, this.id, acct); for (const credit of credits) { const coin = credit.coin; @@ -2438,17 +2264,17 @@ TXDB.prototype.getAccountBalance = async function getAccountBalance(account) { /** * Zap pending transactions older than `age`. - * @param {Number?} account + * @param {Number} acct * @param {Number} age - Age delta (delete transactions older than `now - age`). * @returns {Promise} */ -TXDB.prototype.zap = async function zap(account, age) { +TXDB.prototype.zap = async function zap(acct, age) { assert(util.isU32(age)); const now = util.now(); - const txs = await this.getRange(account, { + const txs = await this.getRange(acct, { start: 0, end: now - age }); @@ -2462,7 +2288,7 @@ TXDB.prototype.zap = async function zap(account, age) { assert(now - wtx.mtime >= age); this.logger.debug('Zapping TX: %s (%s)', - wtx.tx.txid(), this.wallet.id); + wtx.tx.txid(), this.id); await this.remove(wtx.hash); @@ -2787,12 +2613,9 @@ function Details(txdb, wtx, block) { if (!(this instanceof Details)) return new Details(txdb, wtx, block); - this.wallet = txdb.wallet; - this.network = this.wallet.network; - this.wid = this.wallet.wid; - this.id = this.wallet.id; - - this.chainHeight = txdb.wdb.state.height; + this.wid = txdb.wid; + this.id = txdb.id; + this.tip = txdb.wdb.state.height; this.hash = wtx.hash; this.tx = wtx.tx; @@ -2877,7 +2700,7 @@ Details.prototype.getDepth = function getDepth() { if (this.height === -1) return 0; - const depth = this.chainHeight - this.height; + const depth = this.tip - this.height; if (depth < 0) return 0; @@ -2924,13 +2747,9 @@ Details.prototype.getRate = function getRate(fee) { * @returns {Object} */ -Details.prototype.toJSON = function toJSON() { +Details.prototype.toJSON = function toJSON(network) { const fee = this.getFee(); - let rate = this.getRate(fee); - - // Rate can exceed 53 bits in testing. - if (!Number.isSafeInteger(rate)) - rate = 0; + const rate = this.getRate(fee); return { wid: this.wid, @@ -2940,17 +2759,18 @@ Details.prototype.toJSON = function toJSON() { block: this.block ? util.revHex(this.block) : null, time: this.time, mtime: this.mtime, - date: util.date(this.time || this.mtime), + date: util.date(this.time), + mdate: util.date(this.mtime), size: this.size, virtualSize: this.vsize, fee: fee, rate: rate, confirmations: this.getDepth(), inputs: this.inputs.map((input) => { - return input.getJSON(this.network); + return input.getJSON(network); }), outputs: this.outputs.map((output) => { - return output.getJSON(this.network); + return output.getJSON(network); }), tx: this.tx.toRaw().toString('hex') }; diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 5c5ca53f..00d387b3 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -30,7 +30,6 @@ const HD = require('../hd/hd'); const Output = require('../primitives/output'); const Account = require('./account'); const MasterKey = require('./masterkey'); -const LRU = require('../utils/lru'); const policy = require('../protocol/policy'); const consensus = require('../protocol/consensus'); const Mnemonic = HD.Mnemonic; @@ -70,15 +69,11 @@ function Wallet(wdb, options) { assert(wdb, 'WDB required.'); this.wdb = wdb; + this.db = wdb.db; this.network = wdb.network; this.logger = wdb.logger; - this.readLock = new MappedLock(); this.writeLock = new Lock(); this.fundLock = new Lock(); - this.indexCache = new LRU(1000); - this.accountCache = new LRU(1000); - this.pathCache = new LRU(1000); - this.current = null; this.wid = 0; this.id = null; @@ -89,7 +84,7 @@ function Wallet(wdb, options) { this.tokenDepth = 0; this.master = new MasterKey(); - this.txdb = new TXDB(this); + this.txdb = new TXDB(this.wdb); this.account = null; if (options) @@ -207,7 +202,7 @@ Wallet.prototype.init = async function init(options) { this.logger.info('Wallet initialized (%s).', this.id); - await this.txdb.open(); + await this.txdb.open(this); }; /** @@ -227,7 +222,7 @@ Wallet.prototype.open = async function open() { this.logger.info('Wallet opened (%s).', this.id); - await this.txdb.open(); + await this.txdb.open(this); }; /** @@ -239,9 +234,7 @@ Wallet.prototype.destroy = async function destroy() { const unlock1 = await this.writeLock.lock(); const unlock2 = await this.fundLock.lock(); try { - this.wdb.unregister(this); await this.master.destroy(); - this.readLock.destroy(); this.writeLock.destroy(); this.fundLock.destroy(); } finally { @@ -276,30 +269,14 @@ Wallet.prototype.addSharedKey = async function addSharedKey(acct, key) { */ Wallet.prototype._addSharedKey = async function _addSharedKey(acct, key) { - if (!key) { - key = acct; - acct = null; - } - - if (acct == null) - acct = 0; - const account = await this.getAccount(acct); if (!account) throw new Error('Account not found.'); - this.start(); - - let result; - try { - result = await account.addSharedKey(key); - } catch (e) { - this.drop(); - throw e; - } - - await this.commit(); + const b = this.db.batch(); + const result = await account.addSharedKey(b, key); + await b.write(); return result; }; @@ -329,47 +306,30 @@ Wallet.prototype.removeSharedKey = async function removeSharedKey(acct, key) { */ Wallet.prototype._removeSharedKey = async function _removeSharedKey(acct, key) { - if (!key) { - key = acct; - acct = null; - } - - if (acct == null) - acct = 0; - const account = await this.getAccount(acct); if (!account) throw new Error('Account not found.'); - this.start(); - - let result; - try { - result = await account.removeSharedKey(key); - } catch (e) { - this.drop(); - throw e; - } - - await this.commit(); + const b = this.db.batch(); + const result = await account.removeSharedKey(b, key); + await b.write(); return result; }; /** * Change or set master key's passphrase. - * @param {(String|Buffer)?} old - * @param {String|Buffer} new_ + * @param {String|Buffer} passphrase + * @param {String|Buffer} old * @returns {Promise} */ -Wallet.prototype.setPassphrase = async function setPassphrase(old, new_) { - if (old) +Wallet.prototype.setPassphrase = async function setPassphrase(passphrase, old) { + if (old != null) await this.decrypt(old); - if (new_) - await this.encrypt(new_); + await this.encrypt(passphrase); }; /** @@ -396,22 +356,17 @@ Wallet.prototype.encrypt = async function encrypt(passphrase) { Wallet.prototype._encrypt = async function _encrypt(passphrase) { const key = await this.master.encrypt(passphrase, true); - - this.start(); + const b = this.db.batch(); try { - await this.wdb.encryptKeys(this, key); - } catch (e) { + await this.wdb.encryptKeys(b, this.wid, key); + } finally { cleanse(key); - this.drop(); - throw e; } - cleanse(key); + this.save(b); - this.save(); - - await this.commit(); + await b.write(); }; /** @@ -438,22 +393,17 @@ Wallet.prototype.decrypt = async function decrypt(passphrase) { Wallet.prototype._decrypt = async function _decrypt(passphrase) { const key = await this.master.decrypt(passphrase, true); - - this.start(); + const b = this.db.batch(); try { - await this.wdb.decryptKeys(this, key); - } catch (e) { + await this.wdb.decryptKeys(b, this.wid, key); + } finally { cleanse(key); - this.drop(); - throw e; } - cleanse(key); + this.save(b); - this.save(); - - await this.commit(); + await b.write(); }; /** @@ -479,15 +429,16 @@ Wallet.prototype.retoken = async function retoken(passphrase) { */ Wallet.prototype._retoken = async function _retoken(passphrase) { - await this.unlock(passphrase); + if (passphrase) + await this.unlock(passphrase); - this.tokenDepth++; + this.tokenDepth += 1; this.token = this.getToken(this.tokenDepth); - this.start(); - this.save(); + const b = this.db.batch(); + this.save(b); - await this.commit(); + await b.write(); return this.token; }; @@ -546,24 +497,11 @@ Wallet.prototype._renameAccount = async function _renameAccount(acct, name) { if (await this.hasAccount(name)) throw new Error('Account name not available.'); - const old = account.name; + const b = this.db.batch(); - this.start(); + this.wdb.renameAccount(b, account, name); - this.wdb.renameAccount(account, name); - - await this.commit(); - - this.indexCache.remove(old); - - const paths = this.pathCache.values(); - - for (const path of paths) { - if (path.account !== account.accountIndex) - continue; - - path.name = name; - } + await b.write(); }; /** @@ -631,7 +569,8 @@ Wallet.prototype.getID = function getID() { */ Wallet.prototype.getToken = function getToken(nonce) { - assert(this.master.key, 'Cannot derive token.'); + if (!this.master.key) + throw new Error('Cannot derive token.'); const key = this.master.key.derive(44, true); @@ -706,17 +645,11 @@ Wallet.prototype._createAccount = async function _createAccount(options, passphr keys: options.keys }; - this.start(); + const b = this.db.batch(); - let account; - try { - account = Account.fromOptions(this.wdb, opt); - account.wallet = this; - await account.init(); - } catch (e) { - this.drop(); - throw e; - } + const account = Account.fromOptions(this.wdb, opt); + + await account.init(b); this.logger.info('Created account %s/%s/%d.', account.id, @@ -724,9 +657,9 @@ Wallet.prototype._createAccount = async function _createAccount(options, passphr account.accountIndex); this.accountDepth++; - this.save(); + this.save(b); - await this.commit(); + await b.write(); return account; }; @@ -775,7 +708,11 @@ Wallet.prototype.getAddressHashes = function getAddressHashes(acct) { */ Wallet.prototype.getAccountHashes = async function getAccountHashes(acct) { - const index = await this.ensureIndex(acct, true); + const index = await this.getAccountIndex(acct); + + if (index === -1) + throw new Error('Account not found.'); + return await this.wdb.getAccountHashes(this.wid, index); }; @@ -796,69 +733,51 @@ Wallet.prototype.getAccount = async function getAccount(acct) { if (index === -1) return null; - const unlock = await this.readLock.lock(index); - - try { - return await this._getAccount(index); - } finally { - unlock(); - } -}; - -/** - * Retrieve an account from the database without a lock. - * @param {Number} index - * @returns {Promise} - Returns {@link Account}. - */ - -Wallet.prototype._getAccount = async function _getAccount(index) { - const cache = this.accountCache.get(index); - - if (cache) - return cache; - const account = await this.wdb.getAccount(this.wid, index); if (!account) return null; - account.wallet = this; account.wid = this.wid; account.id = this.id; account.watchOnly = this.watchOnly; await account.open(); - this.accountCache.set(index, account); - return account; }; /** * Lookup the corresponding account name's index. - * @param {WalletID} wid - * @param {String|Number} name - Account name/index. + * @param {String|Number} acct - Account name/index. * @returns {Promise} - Returns Number. */ -Wallet.prototype.getAccountIndex = async function getAccountIndex(name) { - if (name == null) +Wallet.prototype.getAccountIndex = function getAccountIndex(acct) { + if (acct == null) return -1; - if (typeof name === 'number') - return name; + if (typeof acct === 'number') + return acct; - const cache = this.indexCache.get(name); + return this.wdb.getAccountIndex(this.wid, acct); +}; - if (cache != null) - return cache; +/** + * Lookup the corresponding account name's index. + * @param {String|Number} acct - Account name/index. + * @returns {Promise} - Returns Number. + * @throws on non-existent account + */ - const index = await this.wdb.getAccountIndex(this.wid, name); +Wallet.prototype.ensureIndex = async function ensureIndex(acct) { + if (acct == null || acct === -1) + return -1; + + const index = await this.getAccountIndex(acct); if (index === -1) - return -1; - - this.indexCache.set(name, index); + throw new Error('Account not found.'); return index; }; @@ -874,11 +793,6 @@ Wallet.prototype.getAccountName = async function getAccountName(index) { if (typeof index === 'string') return index; - const account = this.accountCache.get(index); - - if (account) - return account.name; - return await this.wdb.getAccountName(this.wid, index); }; @@ -894,9 +808,6 @@ Wallet.prototype.hasAccount = async function hasAccount(acct) { if (index === -1) return false; - if (this.accountCache.has(index)) - return true; - return await this.db.hasAccount(this.wid, index); }; @@ -906,7 +817,7 @@ Wallet.prototype.hasAccount = async function hasAccount(acct) { * @returns {Promise} - Returns {@link WalletKey}. */ -Wallet.prototype.createReceive = function createReceive(acct) { +Wallet.prototype.createReceive = function createReceive(acct = 0) { return this.createKey(acct, 0); }; @@ -916,7 +827,7 @@ Wallet.prototype.createReceive = function createReceive(acct) { * @returns {Promise} - Returns {@link WalletKey}. */ -Wallet.prototype.createChange = function createChange(acct) { +Wallet.prototype.createChange = function createChange(acct = 0) { return this.createKey(acct, 1); }; @@ -926,7 +837,7 @@ Wallet.prototype.createChange = function createChange(acct) { * @returns {Promise} - Returns {@link WalletKey}. */ -Wallet.prototype.createNested = function createNested(acct) { +Wallet.prototype.createNested = function createNested(acct = 0) { return this.createKey(acct, 2); }; @@ -955,32 +866,16 @@ Wallet.prototype.createKey = async function createKey(acct, branch) { */ Wallet.prototype._createKey = async function _createKey(acct, branch) { - if (branch == null) { - branch = acct; - acct = null; - } - - if (acct == null) - acct = 0; - const account = await this.getAccount(acct); if (!account) throw new Error('Account not found.'); - this.start(); + const b = this.db.batch(); + const key = await account.createKey(b, branch); + await b.write(); - let result; - try { - result = await account.createKey(branch); - } catch (e) { - this.drop(); - throw e; - } - - await this.commit(); - - return result; + return key; }; /** @@ -989,44 +884,8 @@ Wallet.prototype._createKey = async function _createKey(acct, branch) { * @returns {Promise} */ -Wallet.prototype.save = function save() { - return this.wdb.save(this); -}; - -/** - * Start batch. - * @private - */ - -Wallet.prototype.start = function start() { - return this.wdb.start(this); -}; - -/** - * Drop batch. - * @private - */ - -Wallet.prototype.drop = function drop() { - return this.wdb.drop(this); -}; - -/** - * Clear batch. - * @private - */ - -Wallet.prototype.clear = function clear() { - return this.wdb.clear(this); -}; - -/** - * Save batch. - * @returns {Promise} - */ - -Wallet.prototype.commit = function commit() { - return this.wdb.commit(this); +Wallet.prototype.save = function save(b) { + return this.wdb.save(b, this); }; /** @@ -1048,18 +907,8 @@ Wallet.prototype.hasAddress = async function hasAddress(address) { */ Wallet.prototype.getPath = async function getPath(address) { - const path = await this.readPath(address); - - if (!path) - return null; - - path.name = await this.getAccountName(path.account); - - assert(path.name); - - this.pathCache.set(path.hash, path); - - return path; + const hash = Address.getHash(address, 'hex'); + return this.wdb.getPath(this.wid, this.id, hash); }; /** @@ -1071,19 +920,7 @@ Wallet.prototype.getPath = async function getPath(address) { Wallet.prototype.readPath = async function readPath(address) { const hash = Address.getHash(address, 'hex'); - const cache = this.pathCache.get(hash); - - if (cache) - return cache; - - const path = await this.wdb.getPath(this.wid, hash); - - if (!path) - return null; - - path.id = this.id; - - return path; + return this.wdb.readPath(this.wid, this.id, hash); }; /** @@ -1094,10 +931,6 @@ Wallet.prototype.readPath = async function readPath(address) { Wallet.prototype.hasPath = async function hasPath(address) { const hash = Address.getHash(address, 'hex'); - - if (this.pathCache.has(hash)) - return true; - return await this.wdb.hasPath(this.wid, hash); }; @@ -1120,8 +953,6 @@ Wallet.prototype.getPaths = async function getPaths(acct) { assert(path.name); - this.pathCache.set(path.hash, path); - result.push(path); } @@ -1135,7 +966,11 @@ Wallet.prototype.getPaths = async function getPaths(acct) { */ Wallet.prototype.getAccountPaths = async function getAccountPaths(acct) { - const index = await this.ensureIndex(acct, true); + const index = await this.getAccountIndex(acct); + + if (index === -1) + throw new Error('Account not found.'); + const hashes = await this.getAccountHashes(index); const name = await this.getAccountName(acct); @@ -1151,8 +986,6 @@ Wallet.prototype.getAccountPaths = async function getAccountPaths(acct) { path.name = name; - this.pathCache.set(path.hash, path); - result.push(path); } @@ -1187,15 +1020,6 @@ Wallet.prototype.importKey = async function importKey(acct, ring, passphrase) { */ Wallet.prototype._importKey = async function _importKey(acct, ring, passphrase) { - if (acct && typeof acct === 'object') { - passphrase = ring; - ring = acct; - acct = null; - } - - if (acct == null) - acct = 0; - assert(ring.network === this.network, 'Network mismatch for key.'); @@ -1231,16 +1055,9 @@ Wallet.prototype._importKey = async function _importKey(acct, ring, passphrase) path.encrypted = true; } - this.start(); - - try { - await account.savePath(path); - } catch (e) { - this.drop(); - throw e; - } - - await this.commit(); + const b = this.db.batch(); + await account.savePath(b, path); + await b.write(); }; /** @@ -1271,14 +1088,6 @@ Wallet.prototype.importAddress = async function importAddress(acct, address) { */ Wallet.prototype._importAddress = async function _importAddress(acct, address) { - if (!address) { - address = acct; - acct = null; - } - - if (acct == null) - acct = 0; - assert(address.network === this.network, 'Network mismatch for address.'); @@ -1298,16 +1107,9 @@ Wallet.prototype._importAddress = async function _importAddress(acct, address) { const path = Path.fromAddress(account, address); - this.start(); - - try { - await account.savePath(path); - } catch (e) { - this.drop(); - throw e; - } - - await this.commit(); + const b = this.db.batch(); + await account.savePath(b, path); + await b.write(); }; /** @@ -1881,29 +1683,14 @@ Wallet.prototype.setLookahead = async function setLookahead(acct, lookahead) { */ Wallet.prototype._setLookahead = async function _setLookahead(acct, lookahead) { - if (lookahead == null) { - lookahead = acct; - acct = null; - } - - if (acct == null) - acct = 0; - const account = await this.getAccount(acct); if (!account) throw new Error('Account not found.'); - this.start(); - - try { - await account.setLookahead(lookahead); - } catch (e) { - this.drop(); - throw e; - } - - await this.commit(); + const b = this.db.batch(); + await account.setLookahead(b, lookahead); + await b.write(); }; /** @@ -1933,6 +1720,7 @@ Wallet.prototype.syncOutputDepth = async function syncOutputDepth(tx) { } const derived = []; + const b = this.db.batch(); for (const [acct, paths] of map) { let receive = -1; @@ -1963,33 +1751,17 @@ Wallet.prototype.syncOutputDepth = async function syncOutputDepth(tx) { const account = await this.getAccount(acct); assert(account); - const ring = await account.syncDepth(receive, change, nested); + const ring = await account.syncDepth(b, receive, change, nested); if (ring) derived.push(ring); } + await b.write(); + return derived; }; -/** - * Get a redeem script or witness script by hash. - * @param {Hash} hash - Can be a ripemd160 or a sha256. - * @returns {Script} - */ - -Wallet.prototype.getRedeem = async function getRedeem(hash) { - if (typeof hash === 'string') - hash = Buffer.from(hash, 'hex'); - - const ring = await this.getKey(hash.toString('hex')); - - if (!ring) - return null; - - return ring.getRedeem(hash); -}; - /** * Build input scripts templates for a transaction (does not * sign, only creates signature slots). Only builds scripts @@ -2021,7 +1793,7 @@ Wallet.prototype.sign = async function sign(mtx, passphrase) { const rings = await this.deriveInputs(mtx); - return await mtx.signAsync(rings, Script.hashType.ALL, this.wdb.workers); + return mtx.signAsync(rings, Script.hashType.ALL, this.wdb.workers); }; /** @@ -2128,23 +1900,14 @@ Wallet.prototype.add = async function add(tx, block) { */ Wallet.prototype._add = async function _add(tx, block) { - this.txdb.start(); + const details = await this.txdb.add(tx, block); - let details, derived; - try { - details = await this.txdb._add(tx, block); - if (details) - derived = await this.syncOutputDepth(tx); - } catch (e) { - this.txdb.drop(); - throw e; - } - - await this.txdb.commit(); - - if (derived && derived.length > 0) { - this.wdb.emit('address', this.id, derived); - this.emit('address', derived); + if (details) { + const derived = await this.syncOutputDepth(tx); + if (derived.length > 0) { + this.wdb.emit('address', this.id, derived); + this.emit('address', derived); + } } return details; @@ -2375,10 +2138,6 @@ Wallet.prototype.getBalance = async function getBalance(acct) { */ Wallet.prototype.getRange = async function getRange(acct, options) { - if (acct && typeof acct === 'object') { - options = acct; - acct = null; - } const account = await this.ensureIndex(acct); return await this.txdb.getRange(account, options); }; @@ -2395,29 +2154,6 @@ Wallet.prototype.getLast = async function getLast(acct, limit) { return await this.txdb.getLast(account, limit); }; -/** - * Resolve account index. - * @private - * @param {(Number|String)?} acct - * @param {Function} errback - Returns [Error]. - * @returns {Promise} - */ - -Wallet.prototype.ensureIndex = async function ensureIndex(acct, enforce) { - if (acct == null) { - if (enforce) - throw new Error('No account provided.'); - return null; - } - - const index = await this.getAccountIndex(acct); - - if (index === -1) - throw new Error('Account not found.'); - - return index; -}; - /** * Get current receive address. * @param {String?} enc - `"base58"` or `null`. diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 963101f1..4ef1c468 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -184,8 +184,10 @@ WalletDB.prototype._close = async function _close() { if (this.http && this.options.listen) await this.http.close(); - for (const wallet of this.wallets.values()) + for (const wallet of this.wallets.values()) { await wallet.destroy(); + this.unregister(wallet); + } await this.db.close(); @@ -303,14 +305,14 @@ WalletDB.prototype.disconnect = async function disconnect() { */ WalletDB.prototype.init = async function init() { - const state = await this.getState(); + const cache = await this.getState(); - if (state) { - this.state = state; + if (cache) { + this.state = cache; return; } - const batch = this.db.batch(); + const b = this.db.batch(); let tip = null; @@ -320,27 +322,27 @@ WalletDB.prototype.init = async function init() { for (let height = 0; height < hashes.length; height++) { const hash = hashes[height]; const meta = new BlockMeta(hash, height); - batch.put(layout.h(height), meta.toHash()); + b.put(layout.h(height), meta.toHash()); tip = meta; } } else { tip = new BlockMeta(this.network.genesis.hash, 0); - batch.put(layout.h(0), tip.toHash()); + b.put(layout.h(0), tip.toHash()); } assert(tip); - const pending = this.state.clone(); - pending.startHeight = tip.height; - pending.startHash = tip.hash; - pending.height = tip.height; - pending.marked = false; + const state = this.state.clone(); + state.startHeight = tip.height; + state.startHash = tip.hash; + state.height = tip.height; + state.marked = false; - batch.put(layout.R, pending.toRaw()); + b.put(layout.R, state.toRaw()); - await batch.write(); + await b.write(); - this.state = pending; + this.state = state; }; /** @@ -587,7 +589,7 @@ WalletDB.prototype.wipe = async function wipe() { lte: Buffer.from([0xff]) }); - const batch = this.db.batch(); + const b = this.db.batch(); let total = 0; @@ -600,7 +602,7 @@ WalletDB.prototype.wipe = async function wipe() { case 0x6f: // o case 0x68: // h case 0x52: // R - batch.del(key); + b.del(key); total += 1; break; } @@ -608,7 +610,7 @@ WalletDB.prototype.wipe = async function wipe() { this.logger.warning('Wiped %d txdb records.', total); - await batch.write(); + await b.write(); }; /** @@ -646,83 +648,6 @@ WalletDB.prototype.getDepth = async function getDepth() { return depth + 1; }; -/** - * Start batch. - * @private - * @param {WalletID} wid - */ - -WalletDB.prototype.start = function start(wallet) { - assert(!wallet.current, 'WDB: Batch already started.'); - wallet.current = this.db.batch(); - wallet.accountCache.start(); - wallet.pathCache.start(); - return wallet.current; -}; - -/** - * Drop batch. - * @private - * @param {WalletID} wid - */ - -WalletDB.prototype.drop = function drop(wallet) { - const batch = this.batch(wallet); - wallet.current = null; - wallet.accountCache.drop(); - wallet.pathCache.drop(); - batch.clear(); -}; - -/** - * Clear batch. - * @private - * @param {WalletID} wid - */ - -WalletDB.prototype.clear = function clear(wallet) { - const batch = this.batch(wallet); - wallet.accountCache.clear(); - wallet.pathCache.clear(); - batch.clear(); -}; - -/** - * Get batch. - * @private - * @param {WalletID} wid - * @returns {Leveldown.Batch} - */ - -WalletDB.prototype.batch = function batch(wallet) { - assert(wallet.current, 'WDB: Batch does not exist.'); - return wallet.current; -}; - -/** - * Save batch. - * @private - * @param {WalletID} wid - * @returns {Promise} - */ - -WalletDB.prototype.commit = async function commit(wallet) { - const batch = this.batch(wallet); - - try { - await batch.write(); - } catch (e) { - wallet.current = null; - wallet.accountCache.drop(); - wallet.pathCache.drop(); - throw e; - } - - wallet.current = null; - wallet.accountCache.commit(); - wallet.pathCache.commit(); -}; - /** * Test the bloom filter against a tx or address hash. * @private @@ -793,7 +718,7 @@ WalletDB.prototype.unregister = function unregister(wallet) { * @returns {Promise} - Returns {WalletID}. */ -WalletDB.prototype.getWalletID = async function getWalletID(id) { +WalletDB.prototype.getWID = async function getWID(id) { if (!id) return null; @@ -817,7 +742,7 @@ WalletDB.prototype.getWalletID = async function getWalletID(id) { */ WalletDB.prototype.get = async function get(id) { - const wid = await this.getWalletID(id); + const wid = await this.getWID(id); if (!wid) return null; @@ -863,13 +788,12 @@ WalletDB.prototype._get = async function _get(wid) { * @param {Wallet} wallet */ -WalletDB.prototype.save = function save(wallet) { +WalletDB.prototype.save = function save(b, wallet) { const wid = wallet.wid; const id = wallet.id; - const batch = this.batch(wallet); - batch.put(layout.w(wid), wallet.toRaw()); - batch.put(layout.l(id), U32(wid)); + b.put(layout.w(wid), wallet.toRaw()); + b.put(layout.l(id), U32(wid)); }; /** @@ -897,27 +821,23 @@ WalletDB.prototype.rename = async function rename(wallet, id) { */ WalletDB.prototype._rename = async function _rename(wallet, id) { - const old = wallet.id; - if (!common.isName(id)) throw new Error('WDB: Bad wallet ID.'); if (await this.has(id)) throw new Error('WDB: ID not available.'); - const batch = this.start(wallet); - batch.del(layout.l(old)); + const old = wallet.id; + const b = this.db.batch(); + + b.del(layout.l(old)); wallet.id = id; + wallet.txdb.id = id; - this.save(wallet); + this.save(b, wallet); - await this.commit(wallet); - - const paths = wallet.pathCache.values(); - - for (const path of paths) - path.id = id; + await b.write(); }; /** @@ -926,16 +846,13 @@ WalletDB.prototype._rename = async function _rename(wallet, id) { * @param {String} name */ -WalletDB.prototype.renameAccount = function renameAccount(account, name) { - const wallet = account.wallet; - const batch = this.batch(wallet); - +WalletDB.prototype.renameAccount = function renameAccount(b, account, name) { // Remove old wid/name->account index. - batch.del(layout.i(account.wid, account.name)); + b.del(layout.i(account.wid, account.name)); account.name = name; - this.saveAccount(account); + this.saveAccount(b, account); }; /** @@ -1018,7 +935,7 @@ WalletDB.prototype._create = async function _create(options) { */ WalletDB.prototype.has = async function has(id) { - const wid = await this.getWalletID(id); + const wid = await this.getWID(id); return wid != null; }; @@ -1106,23 +1023,19 @@ WalletDB.prototype.getAccountName = async function getAccountName(wid, index) { * @returns {Promise} */ -WalletDB.prototype.saveAccount = function saveAccount(account) { +WalletDB.prototype.saveAccount = function saveAccount(b, account) { const wid = account.wid; - const wallet = account.wallet; const index = account.accountIndex; const name = account.name; - const batch = this.batch(wallet); // Account data - batch.put(layout.a(wid, index), account.toRaw()); + b.put(layout.a(wid, index), account.toRaw()); // Name->Index lookups - batch.put(layout.i(wid, name), U32(index)); + b.put(layout.i(wid, name), U32(index)); // Index->Name lookups - batch.put(layout.n(wid, index), Buffer.from(name, 'ascii')); - - wallet.accountCache.push(index, account); + b.put(layout.n(wid, index), Buffer.from(name, 'ascii')); }; /** @@ -1143,8 +1056,8 @@ WalletDB.prototype.hasAccount = function hasAccount(wid, index) { * @returns {Promise} */ -WalletDB.prototype.saveKey = function saveKey(wallet, ring) { - return this.savePath(wallet, ring.toPath()); +WalletDB.prototype.saveKey = function saveKey(b, wid, ring) { + return this.savePath(b, wid, ring.toPath()); }; /** @@ -1160,21 +1073,15 @@ WalletDB.prototype.saveKey = function saveKey(wallet, ring) { * @returns {Promise} */ -WalletDB.prototype.savePath = async function savePath(wallet, path) { - const wid = wallet.wid; - const hash = path.hash; - const batch = this.batch(wallet); - - wallet.pathCache.push(hash, path); - +WalletDB.prototype.savePath = async function savePath(b, wid, path) { // Address Hash -> Wallet Map - await this.addPathMap(wallet, hash); + await this.addPathMap(b, path.hash, wid); // Wallet ID + Address Hash -> Path Data - batch.put(layout.P(wid, hash), path.toRaw()); + b.put(layout.P(wid, path.hash), path.toRaw()); // Wallet ID + Account Index + Address Hash -> Dummy - batch.put(layout.r(wid, path.account, hash), null); + b.put(layout.r(wid, path.account, path.hash), null); }; /** @@ -1184,13 +1091,33 @@ WalletDB.prototype.savePath = async function savePath(wallet, path) { * @returns {Promise} */ -WalletDB.prototype.getPath = async function getPath(wid, hash) { +WalletDB.prototype.getPath = async function getPath(wid, id, hash) { + const path = await this.readPath(wid, id, hash); + + if (!path) + return null; + + path.name = await this.getAccountName(wid, path.account); + assert(path.name); + + return path; +}; + +/** + * Retrieve path by hash. + * @param {WalletID} wid + * @param {Hash} hash + * @returns {Promise} + */ + +WalletDB.prototype.readPath = async function readPath(wid, id, hash) { const data = await this.db.get(layout.P(wid, hash)); if (!data) return null; const path = Path.fromRaw(data); + path.id = id; path.wid = wid; path.hash = hash; @@ -1313,28 +1240,30 @@ WalletDB.prototype.getWallets = function getWallets() { * @returns {Promise} */ -WalletDB.prototype.encryptKeys = async function encryptKeys(wallet, key) { - const wid = wallet.wid; - const paths = await wallet.getPaths(); - const batch = this.batch(wallet); +WalletDB.prototype.encryptKeys = async function encryptKeys(b, wid, key) { + const iter = this.db.iterator({ + gte: layout.P(wid, encoding.NULL_HASH), + lte: layout.P(wid, encoding.HIGH_HASH), + values: true + }); + + await iter.each((k, value) => { + const hash = layout.Pp(k); + const path = Path.fromRaw(value); - for (let path of paths) { if (!path.data) - continue; + return; assert(!path.encrypted); - const hash = Buffer.from(path.hash, 'hex'); - const iv = hash.slice(0, 16); + const bhash = Buffer.from(hash, 'hex'); + const iv = bhash.slice(0, 16); - path = path.clone(); path.data = aes.encipher(path.data, key, iv); path.encrypted = true; - wallet.pathCache.push(path.hash, path); - - batch.put(layout.P(wid, path.hash), path.toRaw()); - } + b.put(k, path.toRaw()); + }); }; /** @@ -1344,28 +1273,30 @@ WalletDB.prototype.encryptKeys = async function encryptKeys(wallet, key) { * @returns {Promise} */ -WalletDB.prototype.decryptKeys = async function decryptKeys(wallet, key) { - const wid = wallet.wid; - const paths = await wallet.getPaths(); - const batch = this.batch(wallet); +WalletDB.prototype.decryptKeys = async function decryptKeys(b, wid, key) { + const iter = this.db.iterator({ + gte: layout.P(wid, encoding.NULL_HASH), + lte: layout.P(wid, encoding.HIGH_HASH), + values: true + }); + + await iter.each((k, value) => { + const hash = layout.Pp(k); + const path = Path.fromRaw(value); - for (let path of paths) { if (!path.data) - continue; + return; assert(path.encrypted); - const hash = Buffer.from(path.hash, 'hex'); - const iv = hash.slice(0, 16); + const bhash = Buffer.from(hash, 'hex'); + const iv = bhash.slice(0, 16); - path = path.clone(); path.data = aes.decipher(path.data, key, iv); path.encrypted = false; - wallet.pathCache.push(path.hash, path); - - batch.put(layout.P(wid, path.hash), path.toRaw()); - } + b.put(k, path.toRaw()); + }); }; /** @@ -1500,14 +1431,14 @@ WalletDB.prototype.getState = async function getState() { */ WalletDB.prototype.syncState = async function syncState(tip) { - const batch = this.db.batch(); + const b = this.db.batch(); const state = this.state.clone(); if (tip.height < state.height) { // Hashes ahead of our new tip // that we need to delete. while (state.height !== tip.height) { - batch.del(layout.h(state.height)); + b.del(layout.h(state.height)); state.height -= 1; } } else if (tip.height > state.height) { @@ -1522,10 +1453,10 @@ WalletDB.prototype.syncState = async function syncState(tip) { } // Save tip and state. - batch.put(layout.h(tip.height), tip.toHash()); - batch.put(layout.R, state.toRaw()); + b.put(layout.h(tip.height), tip.toHash()); + b.put(layout.R, state.toRaw()); - await batch.write(); + await b.write(); this.state = state; }; @@ -1542,9 +1473,9 @@ WalletDB.prototype.markState = async function markState(block) { state.startHash = block.hash; state.marked = true; - const batch = this.db.batch(); - batch.put(layout.R, state.toRaw()); - await batch.write(); + const b = this.db.batch(); + b.put(layout.R, state.toRaw()); + await b.write(); this.state = state; }; @@ -1571,15 +1502,13 @@ WalletDB.prototype.getMap = async function getMap(key) { * @param {Number} wid */ -WalletDB.prototype.addMap = async function addMap(wallet, key) { - const wid = wallet.wid; - const batch = this.batch(wallet); +WalletDB.prototype.addMap = async function addMap(b, key, wid) { const data = await this.db.get(key); if (!data) { const map = new MapRecord(); map.add(wid); - batch.put(key, map.toRaw()); + b.put(key, map.toRaw()); return; } @@ -1592,7 +1521,7 @@ WalletDB.prototype.addMap = async function addMap(wallet, key) { bw.copy(data, 4, data.length); bw.writeU32(wid); - batch.put(key, bw.render()); + b.put(key, bw.render()); }; /** @@ -1602,9 +1531,7 @@ WalletDB.prototype.addMap = async function addMap(wallet, key) { * @param {Number} wid */ -WalletDB.prototype.removeMap = async function removeMap(wallet, key) { - const wid = wallet.wid; - const batch = this.batch(wallet); +WalletDB.prototype.removeMap = async function removeMap(b, key, wid) { const map = await this.getMap(key); if (!map) @@ -1614,11 +1541,11 @@ WalletDB.prototype.removeMap = async function removeMap(wallet, key) { return; if (map.size === 0) { - batch.del(key); + b.del(key); return; } - batch.put(key, map.toRaw()); + b.put(key, map.toRaw()); }; /** @@ -1638,9 +1565,9 @@ WalletDB.prototype.getPathMap = function getPathMap(hash) { * @param {Number} wid */ -WalletDB.prototype.addPathMap = function addPathMap(wallet, hash) { +WalletDB.prototype.addPathMap = function addPathMap(b, hash, wid) { this.addHash(hash); - return this.addMap(wallet, layout.p(hash)); + return this.addMap(b, layout.p(hash), wid); }; /** @@ -1650,8 +1577,8 @@ WalletDB.prototype.addPathMap = function addPathMap(wallet, hash) { * @param {Number} wid */ -WalletDB.prototype.removePathMap = function removePathMap(wallet, hash) { - return this.removeMap(wallet, layout.p(hash)); +WalletDB.prototype.removePathMap = function removePathMap(b, hash, wid) { + return this.removeMap(b, layout.p(hash), wid); }; /** @@ -1671,8 +1598,8 @@ WalletDB.prototype.getBlockMap = async function getBlockMap(height) { * @param {Number} wid */ -WalletDB.prototype.addBlockMap = function addBlockMap(wallet, height) { - return this.addMap(wallet, layout.b(height)); +WalletDB.prototype.addBlockMap = function addBlockMap(b, height, wid) { + return this.addMap(b, layout.b(height), wid); }; /** @@ -1682,8 +1609,8 @@ WalletDB.prototype.addBlockMap = function addBlockMap(wallet, height) { * @param {Number} wid */ -WalletDB.prototype.removeBlockMap = function removeBlockMap(wallet, height) { - return this.removeMap(wallet, layout.b(height)); +WalletDB.prototype.removeBlockMap = function removeBlockMap(b, height, wid) { + return this.removeMap(b, layout.b(height), wid); }; /** @@ -1703,8 +1630,8 @@ WalletDB.prototype.getTXMap = function getTXMap(hash) { * @param {Number} wid */ -WalletDB.prototype.addTXMap = function addTXMap(wallet, hash) { - return this.addMap(wallet, layout.T(hash)); +WalletDB.prototype.addTXMap = function addTXMap(b, hash, wid) { + return this.addMap(b, layout.T(hash), wid); }; /** @@ -1714,8 +1641,8 @@ WalletDB.prototype.addTXMap = function addTXMap(wallet, hash) { * @param {Number} wid */ -WalletDB.prototype.removeTXMap = function removeTXMap(wallet, hash) { - return this.removeMap(wallet, layout.T(hash)); +WalletDB.prototype.removeTXMap = function removeTXMap(b, hash, wid) { + return this.removeMap(b, layout.T(hash), wid); }; /** @@ -1724,7 +1651,7 @@ WalletDB.prototype.removeTXMap = function removeTXMap(wallet, hash) { * @returns {Promise} */ -WalletDB.prototype.getOutpointMap = async function getOutpointMap(hash, index) { +WalletDB.prototype.getOutpointMap = function getOutpointMap(hash, index) { return this.getMap(layout.o(hash, index)); }; @@ -1735,9 +1662,9 @@ WalletDB.prototype.getOutpointMap = async function getOutpointMap(hash, index) { * @param {Number} wid */ -WalletDB.prototype.addOutpointMap = async function addOutpointMap(wallet, hash, index) { - this.addOutpoint(hash, index); - return this.addMap(wallet, layout.o(hash, index)); +WalletDB.prototype.addOutpointMap = async function addOutpointMap(b, hash, index, wid) { + await this.addOutpoint(hash, index); + return this.addMap(b, layout.o(hash, index), wid); }; /** @@ -1747,8 +1674,8 @@ WalletDB.prototype.addOutpointMap = async function addOutpointMap(wallet, hash, * @param {Number} wid */ -WalletDB.prototype.removeOutpointMap = async function removeOutpointMap(wallet, hash, index) { - return this.removeMap(wallet, layout.o(hash, index)); +WalletDB.prototype.removeOutpointMap = function removeOutpointMap(b, hash, index, wid) { + return this.removeMap(b, layout.o(hash, index), wid); }; /** diff --git a/test/wallet-test.js b/test/wallet-test.js index 7f78e817..2228a7ae 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -118,14 +118,14 @@ async function testP2SH(witness, nesting) { const carol = await wdb.create(options); const recipient = await wdb.create(); - await alice.addSharedKey(bob.account.accountKey); - await alice.addSharedKey(carol.account.accountKey); + await alice.addSharedKey(0, bob.account.accountKey); + await alice.addSharedKey(0, carol.account.accountKey); - await bob.addSharedKey(alice.account.accountKey); - await bob.addSharedKey(carol.account.accountKey); + await bob.addSharedKey(0, alice.account.accountKey); + await bob.addSharedKey(0, carol.account.accountKey); - await carol.addSharedKey(alice.account.accountKey); - await carol.addSharedKey(bob.account.accountKey); + await carol.addSharedKey(0, alice.account.accountKey); + await carol.addSharedKey(0, bob.account.accountKey); // Our p2sh address const addr1 = alice.account[receive].getAddress(); @@ -262,15 +262,8 @@ describe('Wallet', function() { it('should create and get wallet', async () => { const wallet1 = await wdb.create(); - - await wallet1.destroy(); - const wallet2 = await wdb.get(wallet1.id); - - assert(wallet1 !== wallet2); - assert(wallet1.master !== wallet2.master); - assert(wallet1.master.key.equals(wallet2.master.key)); - assert(wallet1.account.accountKey.equals(wallet2.account.accountKey)); + assert(wallet1 === wallet2); }); it('should sign/verify p2pkh tx', async () => { @@ -295,7 +288,7 @@ describe('Wallet', function() { const xpriv = HD.PrivateKey.generate(); const key = xpriv.deriveAccount(44, 0).toPublic(); - await wallet.addSharedKey(key); + await wallet.addSharedKey(0, key); const script = Script.fromMultisig(1, 2, [ wallet.account.receive.getPublicKey(), @@ -684,7 +677,7 @@ describe('Wallet', function() { assert.strictEqual(t2.getSize(), 521); assert.strictEqual(t2.getVirtualSize(), 521); - let balance; + let balance = null; bob.once('balance', (b) => { balance = b; }); @@ -927,6 +920,7 @@ describe('Wallet', function() { passphrase: 'foo' }); await wallet.destroy(); + wdb.unregister(wallet); } const wallet = await wdb.get('foobar'); @@ -1156,7 +1150,7 @@ describe('Wallet', function() { it('should get range of txs', async () => { const wallet = currentWallet; - const txs = await wallet.getRange({ + const txs = await wallet.getRange(null, { start: util.now() - 1000 }); assert.strictEqual(txs.length, 2);