diff --git a/lib/wallet/layout.js b/lib/wallet/layout.js index 43deba732..c72b862ed 100644 --- a/lib/wallet/layout.js +++ b/lib/wallet/layout.js @@ -94,6 +94,7 @@ exports.wdb = { * ------- * R -> wallet balance * r[account] -> account balance + * I -> Latest Unconfirmed Index * * Coin * ---- @@ -106,19 +107,26 @@ exports.wdb = { * ----------- * t[tx-hash] -> extended tx * T[account][tx-hash] -> dummy (tx by account) - * m[time][tx-hash] -> dummy (tx by time) - * M[account][time][tx-hash] -> dummy (tx by time + account) + * z[height][index] -> tx hash (tx by count) + * Z[account][height][index] -> tx hash (tx by count + account) + * y[hash] -> count (count for tx) + * x[hash] -> undo count (unconfirmed count for tx) * * Confirmed * --------- * b[height] -> block record * h[height][tx-hash] -> dummy (tx by height) * H[account][height][tx-hash] -> dummy (tx by height + account) + * g[time][height][index][hash] -> dummy (tx by time) + * G[account][time][height][index][hash] -> dummy (tx by time + account) * * Unconfirmed * ----------- * p[hash] -> dummy (pending tx) * P[account][tx-hash] -> dummy (pending tx by account) + * w[time][count][hash] -> dummy (tx by time) + * W[account][time][count][hash] -> dummy (tx by time + account) + * e[hash] -> undo time (unconfirmed time for tx) * * Names * ----- @@ -136,6 +144,7 @@ exports.txdb = { // Balance R: bdb.key('R'), r: bdb.key('r', ['uint32']), + I: bdb.key('I'), // Coin c: bdb.key('c', ['hash256', 'uint32']), @@ -148,15 +157,24 @@ exports.txdb = { T: bdb.key('T', ['uint32', 'hash256']), m: bdb.key('m', ['uint32', 'hash256']), M: bdb.key('M', ['uint32', 'uint32', 'hash256']), + z: bdb.key('z', ['uint32', 'uint32']), + Z: bdb.key('Z', ['uint32', 'uint32', 'uint32']), + y: bdb.key('y', ['hash256']), + x: bdb.key('u', ['hash256']), // Confirmed b: bdb.key('b', ['uint32']), h: bdb.key('h', ['uint32', 'hash256']), H: bdb.key('H', ['uint32', 'uint32', 'hash256']), + g: bdb.key('g', ['uint32', 'uint32', 'uint32', 'hash256']), + G: bdb.key('G', ['uint32', 'uint32', 'uint32', 'uint32', 'hash256']), // Unconfirmed p: bdb.key('p', ['hash256']), P: bdb.key('P', ['uint32', 'hash256']), + w: bdb.key('w', ['uint32', 'uint32', 'hash256']), + W: bdb.key('W', ['uint32', 'uint32', 'uint32', 'hash256']), + e: bdb.key('e', ['hash256']), // Names A: bdb.key('A', ['hash256']), diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 63b825f2b..734c43cfe 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -25,6 +25,7 @@ const {TXRecord} = records; const {types} = rules; /** @typedef {import('./records').BlockMeta} BlockMeta */ +/** @typedef {import('./walletdb')} WalletDB */ /** * @typedef {Object} BlockExtraInfo @@ -37,6 +38,7 @@ const {types} = rules; */ const EMPTY = Buffer.alloc(0); +const UNCONFIRMED_HEIGHT = 0xffffffff; /** * TXDB @@ -44,6 +46,9 @@ const EMPTY = Buffer.alloc(0); */ class TXDB { + /** @type {WalletDB} */ + wdb; + /** * Create a TXDB. * @constructor @@ -1107,9 +1112,28 @@ class TXDB { // Update block records. if (block) { + // If confirmed in a block (e.g. coinbase tx) and not + // being updated or previously seen, we need to add + // the monotonic time and count index for the transaction + await this.addCountAndTimeIndex(b, { + accounts: state.accounts, + hash, + height: block.height, + blockextra: extra + }); + + // In the event that this transaction becomes unconfirmed + // during a reorganization, this transaction will need an + // unconfirmed time and unconfirmed index, however since this + // transaction was not previously seen previous to the block, + // we need to add that information. + await this.addTimeAndCountIndexUnconfirmedUndo(b, hash); + await this.addBlockMap(b, height); await this.addBlock(b, tx.hash(), block); } else { + // Add indexing for unconfirmed transactions. + await this.addCountAndTimeIndexUnconfirmed(b, state.accounts, hash); await this.addTXMap(b, hash); } @@ -1150,6 +1174,7 @@ class TXDB { const view = new CoinView(); let own = false; + assert(block && extra); wtx.setBlock(block); if (!tx.isCoinbase()) { @@ -1274,6 +1299,21 @@ class TXDB { // Handle names. await this.connectNames(b, tx, view, height); + // Disconnect unconfirmed time index for the transaction. + // This must go before adding the the indexes, as the + // unconfirmed count needs to be copied first. + await this.disconnectCountAndTimeIndexUnconfirmed(b, state.accounts, hash); + + // Add monotonic and count time index for transactions + // that already exist in the database and are now + // being confirmed. + await this.addCountAndTimeIndex(b, { + accounts: state.accounts, + hash, + height: block.height, + blockextra: extra + }); + // Save the new serialized transaction as // the block-related properties have been // updated. Also reindex for height. @@ -1329,10 +1369,11 @@ class TXDB { * @private * @param {TXRecord} wtx * @param {BlockMeta} [block] + * @param {Number} [medianTime] * @returns {Promise
} */ - async erase(wtx, block) { + async erase(wtx, block, medianTime) { const b = this.bucket.batch(); const {tx, hash} = wtx; const height = block ? block.height : -1; @@ -1442,10 +1483,22 @@ class TXDB { // Update block records. if (block) { + // Remove tx count and time indexing. + await this.removeCountAndTimeIndex(b, { + hash, + medianTime, + accounts: state.accounts + }); + + // We also need to clean up unconfirmed undos. + await this.removeTimeAndCountIndexUnconfirmedUndo(b, hash); + await this.removeBlockMap(b, height); await this.spliceBlock(b, hash, height); } else { await this.removeTXMap(b, hash); + // Remove count and time indexes. + await this.removeCountAndTimeIndexUnconfirmed(b, state.accounts, hash); } // Update the transaction counter @@ -1466,6 +1519,7 @@ class TXDB { * remove all of its spenders. * @private * @param {TXRecord} wtx + * @param {Number} medianTime * @returns {Promise} */ @@ -1489,8 +1543,14 @@ class TXDB { await this.removeRecursive(stx); } + const block = wtx.getBlock(); + let medianTime; + + if (block) + medianTime = await this.wdb.getMedianTime(block.height); + // Remove the spender. - return this.erase(wtx, wtx.getBlock()); + return this.erase(wtx, block, medianTime); } /** @@ -1506,10 +1566,18 @@ class TXDB { return 0; const hashes = block.toArray(); + const mtp = await this.wdb.getMedianTime(height); + assert(mtp); for (let i = hashes.length - 1; i >= 0; i--) { const hash = hashes[i]; - await this.unconfirm(hash, height); + /** @type {BlockExtraInfo} */ + const extra = { + medianTime: mtp, + txIndex: i + }; + + await this.unconfirm(hash, height, extra); } return hashes.length; @@ -1520,10 +1588,11 @@ class TXDB { * @private * @param {Hash} hash * @param {Number} height + * @param {BlockExtraInfo} extra * @returns {Promise} */ - async unconfirm(hash, height) { + async unconfirm(hash, height, extra) { const wtx = await this.getTX(hash); if (!wtx) { @@ -1560,24 +1629,25 @@ class TXDB { if (await this.isDoubleOpen(tx)) return this.removeRecursive(wtx); - return this.disconnect(wtx, wtx.getBlock()); + return this.disconnect(wtx, wtx.getBlock(), extra); } /** * Unconfirm a transaction. Necessary after a reorg. * @param {TXRecord} wtx * @param {BlockMeta} block + * @param {BlockExtraInfo} extra * @returns {Promise
} */ - async disconnect(wtx, block) { + async disconnect(wtx, block, extra) { const b = this.bucket.batch(); const {tx, hash, height} = wtx; const details = new Details(wtx, block); const state = new BalanceDelta(); let own = false; - assert(block); + assert(block && extra); wtx.unsetBlock(); @@ -1702,6 +1772,17 @@ class TXDB { b.del(layout.H.encode(acct, height, hash)); } + // Remove tx count and time indexing. This must + // go before restoring the unconfirmed count. + await this.removeCountAndTimeIndex(b, { + hash, + medianTime: extra.medianTime, + accounts: state.accounts + }); + + // Restore count indexing for unconfirmed txs. + await this.restoreCountAndTimeIndexUnconfirmed(b, state.accounts, hash); + // Commit state due to unconfirmed // vs. confirmed balance change. const balance = await this.updateBalance(b, state); @@ -1714,6 +1795,331 @@ class TXDB { return details; } + /* + * Count and time index. + */ + + /** + * Get the latest unconfirmed TX count from the database. This number + * does not represent the count of current unconfirmed transactions, + * but the count of all unconfirmed transactions. As transactions are + * confirmed the value is deleted, however proceeding values are not + * decremented as to not have a large number of database updates at once. + * @private + * @returns {Promise} + */ + + async getLatestUnconfirmedTXCount() { + const raw = await this.db.get(layout.I.encode()); + let index = 0; + + if (raw) + index = raw.readUInt32LE(0, true); + + return new TXCount(UNCONFIRMED_HEIGHT, index); + } + + /** + * Increment latest unconfirmed index. + * @private + * @param {Batch} b + * @param {Number} count + */ + + incrementLatestUnconfirmedTXCount(b, count) { + assert(count + 1 <= 0xffffffff, 'Number exceeds 32-bits.'); + b.put(layout.I.encode(), fromU32(count + 1)); + } + + /** + * Get the count of a transaction. + * @private + * @param {Hash} hash - Transaction hash. + * @returns {Promise} + */ + + async getCountForTX(hash) { + assert(Buffer.isBuffer(hash)); + + const raw = await this.bucket.get(layout.y.encode(hash)); + + if (!raw) + return null; + + return TXCount.decode(raw); + } + + /** + * Get the undo count of a transaction. + * @private + * @param {Hash} hash - Transaction hash. + * @returns {Promise} + */ + + async getUndoCountForTX(hash) { + assert(Buffer.isBuffer(hash)); + + const raw = await this.bucket.get(layout.x.encode(hash)); + + if (!raw) + return null; + + return TXCount.decode(raw); + } + + /** + * Add undo unconfirmed time and unconfirmed index to restore unconfirmed + * time during reorganizations. + * @private + * @param {Batch} b + * @param {Hash} hash - Transaction hash. + * @returns {Promise} + */ + + async addTimeAndCountIndexUnconfirmedUndo(b, hash) { + const time = util.now(); + const count = await this.getLatestUnconfirmedTXCount(); + + b.put(layout.e.encode(hash), fromU32(time)); + b.put(layout.x.encode(hash), count.encode()); + + this.incrementLatestUnconfirmedTXCount(b, count.index); + } + + /** + * Remove undo unconfirmed time and unconfirmed index. + * @private + * @param {Batch} b + * @param {Hash} hash - Transaction hash. + * @returns {Promise} + */ + + async removeTimeAndCountIndexUnconfirmedUndo(b, hash) { + b.del(layout.e.encode(hash)); + b.del(layout.x.encode(hash)); + } + + /** + * Add unconfirmed time indexing to support querying + * unconfirmed transaction history in subsets by time. + * @private + * @param {Hash} hash - Transaction hash. + * @returns {Promise} + */ + + async getUnconfirmedTimeForTX(hash) { + const raw = await this.bucket.get(layout.e.encode(hash)); + if (!raw) { + throw new Error('Unconfirmed time not found.'); + } + return raw.readUInt32LE(0, true); + } + + /** + * Add count and time based indexing to support querying + * unconfirmed transaction history in subsets by time + * and by count. + * This is called when we see a new transaction that is + * unconfirmed. insert() w/o block. + * @private + * @param {Batch} b + * @param {Map} accounts + * @param {Hash} hash - Transaction hash. + * @returns {Promise} + */ + + async addCountAndTimeIndexUnconfirmed(b, accounts, hash) { + const count = await this.getLatestUnconfirmedTXCount(); + + b.put(layout.z.encode(count.height, count.index), hash); + b.put(layout.y.encode(hash), count.encode()); + + const time = util.now(); + b.put(layout.e.encode(hash), fromU32(time)); + b.put(layout.w.encode(time, count.index, hash)); + + for (const [acct] of accounts) { + b.put(layout.Z.encode(acct, count.height, count.index), hash); + b.put(layout.W.encode(acct, time, count.index, hash)); + } + + this.incrementLatestUnconfirmedTXCount(b, count.index); + } + + /** + * Remove unconfirmed count and time based indexing. This will + * however leave some of the information around so that it's + * possible to restore the index should it be necessary during a + * reorg. This will remove indexing into the subsets of confirmed + * results, it will keep the count in the database that can be + * queried by hash, should there be a reorg and the transaction + * becomes pending again. + * @private + * @param {Batch} b + * @param {Map} accounts + * @param {Hash} hash - Transaction hash. + */ + + async disconnectCountAndTimeIndexUnconfirmed(b, accounts, hash) { + const count = await this.getCountForTX(hash); + + if (!count) + throw new Error('Transaction count not found.'); + + if (count.height !== UNCONFIRMED_HEIGHT) + throw new Error('Transaction is confirmed.'); + + // Add undo information to later restore the + // unconfirmed count, and remove the count. + b.put(layout.x.encode(hash), count.encode()); + + const {height, index} = count; + b.del(layout.z.encode(height, index)); + b.del(layout.y.encode(hash)); + + const time = await this.getUnconfirmedTimeForTX(hash); + b.del(layout.w.encode(time, index, hash)); + + for (const [acct] of accounts) { + b.del(layout.Z.encode(acct, height, index)); + b.del(layout.W.encode(acct, time, index, hash)); + } + } + + /** + * This will restore the count and time indexing for + * unconfirmed transactions during reorganizations. This is + * possible because we leave the pre-existing count in + * the database. + * @private + * @param {Batch} b + * @param {Map} accounts + * @param {Hash} hash - Transaction hash. + */ + + async restoreCountAndTimeIndexUnconfirmed(b, accounts, hash) { + const count = await this.getUndoCountForTX(hash); + + if (!count) + throw new Error('Transaction count not found.'); + + b.put(layout.y.encode(hash), count.encode()); + b.put(layout.z.encode(count.height, count.index), hash); + + const time = await this.getUnconfirmedTimeForTX(hash); + b.put(layout.w.encode(time, count.index, hash)); + + for (const [acct] of accounts) { + b.put(layout.Z.encode(acct, count.height, count.index), hash); + b.put(layout.W.encode(acct, time, count.index, hash)); + } + } + + /** + * Remove all unconfirmed count and time based indexing. + * @private + * @param {Batch} b + * @param {Map} accounts + * @param {Hash} hash - Transaction hash. + * @returns {Promise} + */ + + async removeCountAndTimeIndexUnconfirmed(b, accounts, hash) { + const count = await this.getCountForTX(hash); + + if (!count) + throw new Error('Transaction count not found.'); + + if (count.height !== UNCONFIRMED_HEIGHT) + throw new Error('Transaction is confirmed.'); + + b.del(layout.z.encode(count.height, count.index)); + b.del(layout.y.encode(hash)); + + const time = await this.getUnconfirmedTimeForTX(hash); + b.del(layout.w.encode(time, count.index, hash)); + b.del(layout.e.encode(hash)); + + for (const [acct] of accounts) { + b.del(layout.Z.encode(acct, count.height, count.index)); + b.del(layout.W.encode(acct, time, count.index, hash)); + } + } + + /** + * Add monotonic time and count based indexing to support + * querying transaction history in subsets and by time. + * @private + * @param {Batch} b + * @param {Object} options + * @param {Map} options.accounts + * @param {Hash} options.hash - Transaction hash. + * @param {Number} options.height + * @param {BlockExtraInfo} options.blockextra + * @returns {Promise} + */ + + async addCountAndTimeIndex(b, options) { + const { + accounts, + hash, + height, + blockextra + } = options; + + const index = blockextra.txIndex; + const count = new TXCount(height, index); + + b.put(layout.z.encode(height, index), hash); + b.put(layout.y.encode(hash), count.encode()); + + const time = blockextra.medianTime; + b.put(layout.g.encode(time, height, index, options.hash)); + + for (const [acct] of accounts) { + b.put(layout.Z.encode(acct, height, index), hash); + b.put(layout.G.encode(acct, time, height, index, options.hash)); + } + } + + /** + * Remove monotonic time and count based indexing. + * @private + * @param {Batch} b + * @param {Object} options + * @param {Hash} options.hash - Transaction hash. + * @param {Number} options.medianTime - Block median time. + * @param {Map} options.accounts + * @returns {Promise} + */ + + async removeCountAndTimeIndex(b, options) { + const { + accounts, + hash, + medianTime + } = options; + + const count = await this.getCountForTX(hash); + + if (!count) + throw new Error('Transaction count not found.'); + + b.del(layout.z.encode(count.height, count.index)); + b.del(layout.y.encode(hash)); + + const time = medianTime; + b.del(layout.g.encode(time, count.height, + count.index, options.hash)); + + for (const [acct] of accounts) { + b.del(layout.Z.encode(acct, count.height, count.index)); + + b.del(layout.G.encode(acct, time, count.height, + count.index, options.hash)); + } + } + /** * Remove spenders that have not been confirmed. We do this in the * odd case of stuck transactions or when a coin is double-spent @@ -2658,6 +3064,7 @@ class TXDB { /** * Get TX hashes by timestamp range. + * @deprecated * @param {Number} acct * @param {Object} options * @param {Number} options.start - Start height. @@ -2687,6 +3094,7 @@ class TXDB { /** * Get TX hashes by timestamp range. + * @deprecated * @param {Number} acct * @param {Object} options * @param {Number} options.start - Start height. @@ -2719,6 +3127,7 @@ class TXDB { /** * Get transactions by timestamp range. + * @deprecated * @param {Number} acct * @param {Object} options * @param {Number} options.start - Start time. @@ -2743,6 +3152,7 @@ class TXDB { /** * Get last N transactions. + * @deprecated * @param {Number} acct * @param {Number} limit - Max number of transactions. * @returns {Promise} @@ -3256,6 +3666,7 @@ class TXDB { /** * Zap pending transactions older than `age`. + * @deprecated - Update * @param {Number} acct * @param {Number} age - Age delta. * @returns {Promise} - zapped tx hashes. @@ -4107,6 +4518,78 @@ class BidReveal extends bio.Struct { } } +/** + * TX Count + * + * This is used for tracking the block height and transaction + * index for the wallet. This is used as entry point into + * indexes that are organized by count. + */ + +class TXCount { + /** + * Create tx count record. + * @constructor + * @param {Number} height + * @param {Number} index + */ + + constructor(height, index) { + this.height = height; + this.index = index; + } + + /** + * Serialize. + * @returns {Buffer} + */ + + encode() { + const bw = bio.write(8); + + bw.writeU32(this.height); + bw.writeU32(this.index); + + return bw.render(); + } + + /** + * Deserialize. + * @private + * @param {Buffer} data + */ + + decode(data) { + const br = bio.read(data); + + this.height = br.readU32(); + this.index = br.readU32(); + + return this; + } + + /** + * Instantiate a tx count from a buffer. + * @param {Buffer} data + * @returns {TXCount} + */ + + static decode(data) { + return new this().decode(data); + } +} + +/** + * @param {Number} num + * @returns {Buffer} + */ + +function fromU32(num) { + const data = Buffer.allocUnsafe(4); + data.writeUInt32LE(num, 0, true); + return data; +} + /* * Expose */ @@ -4120,5 +4603,6 @@ TXDB.BlockRecord = BlockRecord; TXDB.BlindBid = BlindBid; TXDB.BlindValue = BlindValue; TXDB.BidReveal = BidReveal; +TXDB.TXCount = TXCount; module.exports = TXDB; diff --git a/test/wallet-coinselection-test.js b/test/wallet-coinselection-test.js index f4cf9e940..2576c1f5b 100644 --- a/test/wallet-coinselection-test.js +++ b/test/wallet-coinselection-test.js @@ -1,12 +1,11 @@ 'use strict'; const assert = require('bsert'); -const { - MTX, - Network, - WalletDB, - policy -} = require('..'); +const util = require('../lib/utils/util'); +const Network = require('../lib/protocol/network'); +const MTX = require('../lib/primitives/mtx'); +const WalletDB = require('../lib/wallet/walletdb'); +const policy = require('../lib/protocol/policy'); // Use main instead of regtest because (deprecated) // CoinSelector.MAX_FEE was network agnostic @@ -27,7 +26,7 @@ async function fundWallet(wallet, amounts) { const dummyBlock = { hash, height, - time: Date.now() + time: util.now() }; await wallet.wdb.addBlock(dummyBlock, [mtx.toTX()]); } diff --git a/test/wallet-test.js b/test/wallet-test.js index 16ecb1d35..de41bb573 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -1758,10 +1758,13 @@ describe('Wallet', function() { const block100 = { height: 100, hash: Buffer.alloc(32, 0), - time: Date.now() + time: util.now() }; const wtx0 = await wallet.txdb.getTX(tx0.hash()); - await wallet.txdb.confirm(wtx0, block100); + await wallet.txdb.confirm(wtx0, block100, { + medianTime: await wdb.getMedianTimeTip(99, block100.time), + txIndex: 0 + }); ancs = await wallet.getPendingAncestors(tx2); assert.strictEqual(ancs.size, 1); @@ -1770,10 +1773,13 @@ describe('Wallet', function() { const block101 = { height: 101, hash: Buffer.alloc(32, 1), - time: Date.now() + time: util.now() }; const wtx1 = await wallet.txdb.getTX(tx1.hash()); - await wallet.txdb.confirm(wtx1, block101); + await wallet.txdb.confirm(wtx1, block101, { + medianTime: await wdb.getMedianTimeTip(100, block101.time), + txIndex: 0 + }); ancs = await wallet.getPendingAncestors(tx2); assert.strictEqual(ancs.size, 0); @@ -1861,14 +1867,16 @@ describe('Wallet', function() { const block = { height: 100, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; // Get TX from txdb const wtx = await wallet.txdb.getTX(hash); - // Confirm TX with dummy block in txdb - const details = await wallet.txdb.confirm(wtx, block); + const details = await wallet.txdb.confirm(wtx, block, { + medianTime: await wdb.getMedianTimeTip(99, block.time), + txIndex: 0 + }); assert.bufferEqual(details.tx.hash(), hash); // Check balance @@ -1917,13 +1925,16 @@ describe('Wallet', function() { const block1 = { height: 99, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; // Get TX from txdb const wtx1 = await wallet.txdb.getTX(tx1.hash()); // Confirm TX with dummy block in txdb - await wallet.txdb.confirm(wtx1, block1); + await wallet.txdb.confirm(wtx1, block1, { + medianTime: await wdb.getMedianTimeTip(98, block1.time), + txIndex: 0 + }); // Build TX to both addresses, known and unknown const mtx2 = new MTX(); @@ -1953,14 +1964,17 @@ describe('Wallet', function() { const block2 = { height: 100, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; // Get TX from txdb const wtx2 = await wallet.txdb.getTX(hash); // Confirm TX with dummy block in txdb - const details = await wallet.txdb.confirm(wtx2, block2); + const details = await wallet.txdb.confirm(wtx2, block2, { + medianTime: await wdb.getMedianTimeTip(99, block2.time), + txIndex: 0 + }); assert.bufferEqual(details.tx.hash(), hash); // Check balance @@ -2182,11 +2196,11 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; // Add confirmed funding TX to wallet - await wallet.txdb.add(tx, block); + await txdbAdd(wallet, tx, block); // Check const bal = await wallet.getBalance(); @@ -2215,9 +2229,9 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(open, block); + await txdbAdd(wallet, open, block); start = wdb.height; cTXCount++; @@ -2251,9 +2265,9 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(bid, block); + await txdbAdd(wallet, bid, block); cTXCount++; // Check @@ -2286,9 +2300,9 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(reveal, block); + await txdbAdd(wallet, reveal, block); cTXCount++; // Check @@ -2321,9 +2335,9 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(mtx.toTX(), block); + await txdbAdd(wallet, mtx.toTX(), block); }); it('should send and confirm REGISTER', async () => { @@ -2349,9 +2363,9 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(register, block); + await txdbAdd(wallet, register, block); cTXCount++; // Check @@ -2390,9 +2404,9 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(transfer, block); + await txdbAdd(wallet, transfer, block); cTXCount++; // Check @@ -2443,10 +2457,12 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(finalize, block); - await recip.txdb.add(finalize, block); + + await txdbAdd(wallet, finalize, block); + await txdbAdd(recip, finalize, block); + finalizeBlock = block.height; cTXCount++; @@ -2479,8 +2495,21 @@ describe('Wallet', function() { }); it('should disconnect FINALIZE', async () => { - await wallet.txdb.revert(finalizeBlock); - await recip.txdb.revert(finalizeBlock); + const walletBlock = await wallet.txdb.getBlock(finalizeBlock); + const walletHashes = walletBlock.toArray(); + assert.strictEqual(walletHashes.length, 1); + await wallet.txdb.unconfirm(walletHashes[0], finalizeBlock, { + medianTime: walletBlock.time, + txIndex: 0 + }); + + const recipBlock = await recip.txdb.getBlock(finalizeBlock); + const recipHashes = recipBlock.toArray(); + assert.strictEqual(recipHashes.length, 1); + await recip.txdb.unconfirm(recipHashes[0], finalizeBlock, { + medianTime: recipBlock.time, + txIndex: 0 + }); cTXCount--; // Check @@ -2548,11 +2577,11 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; // Add confirmed funding TX to wallet - await wallet.txdb.add(tx, block); + await txdbAdd(wallet, tx, block); // Check const bal = await wallet.getBalance(); @@ -2580,9 +2609,10 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(open.toTX(), block); + + await txdbAdd(wallet, open.toTX(), block); start = wdb.height; uTXCount++; cTXCount++; @@ -2616,9 +2646,10 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(bid.toTX(), block); + + await txdbAdd(wallet, bid.toTX(), block); uTXCount++; cTXCount++; @@ -2651,9 +2682,9 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(reveal.toTX(), block); + await txdbAdd(wallet, reveal.toTX(), block); uTXCount++; cTXCount++; @@ -2687,9 +2718,9 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(mtx.toTX(), block); + await txdbAdd(wallet, mtx.toTX(), block); }); it('should confirm new REGISTER', async () => { @@ -2712,9 +2743,10 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(register.toTX(), block); + + await txdbAdd(wallet, register.toTX(), block); uTXCount++; cTXCount++; @@ -2750,9 +2782,9 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(transfer.toTX(), block); + await txdbAdd(wallet, transfer.toTX(), block); uTXCount++; cTXCount++; @@ -2785,9 +2817,9 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(finalize.toTX(), block); + await txdbAdd(wallet, finalize.toTX(), block); uTXCount++; cTXCount++; @@ -3226,11 +3258,11 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; // Add confirmed funding TX to wallet - await wallet.txdb.add(tx, block); + await txdbAdd(wallet, tx, block); // Check const bal = await wallet.getBalance(); @@ -3259,9 +3291,9 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(open, block); + await txdbAdd(wallet, open, block); cTXCount++; // Check @@ -3295,9 +3327,9 @@ describe('Wallet', function() { let block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(losingBid, block); + await txdbAdd(wallet, losingBid, block); cTXCount++; const losingBlindFromMtx = losingBid.outputs @@ -3340,9 +3372,10 @@ describe('Wallet', function() { block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(winningBid, block); + + await txdbAdd(wallet, winningBid, block); cTXCount++; const winningBlindFromMtx = winningBid.outputs @@ -3384,9 +3417,9 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(reveal, block); + await txdbAdd(wallet, reveal, block); cTXCount++; const revealValueFromMtx = reveal.outputs.find(o => o.covenant.isReveal()) @@ -3414,9 +3447,9 @@ describe('Wallet', function() { const block2 = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(reveal2, block2); + await txdbAdd(wallet, reveal2, block2); cTXCount++; const reveal2ValueFromMtx = reveal.outputs.find(o => @@ -3460,9 +3493,9 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(redeem, block); + await txdbAdd(wallet, redeem, block); cTXCount++; // Check @@ -3497,9 +3530,9 @@ describe('Wallet', function() { const block = { height: wdb.height + 1, hash: Buffer.alloc(32), - time: Date.now() + time: util.now() }; - await wallet.txdb.add(register, block); + await txdbAdd(wallet, register, block); cTXCount++; // Check @@ -3645,3 +3678,10 @@ describe('Wallet', function() { }); }); }); + +async function txdbAdd(wallet, tx, block, txIndex = 0) { + return wallet.txdb.add(tx, block, { + medianTime: block.time, + txIndex + }); +};