diff --git a/CHANGELOG.md b/CHANGELOG.md index 74ec55dbd..10475cf02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,12 @@ ## Unreleased -### Wallet API: +**When upgrading to this version of hsd, you must pass `--wallet-migrate=3` when +you run it for the first time.** + +### Wallet Changes: +- Add migration that recalculates txdb balances to fix any inconsistencies. - HTTP Changes: - All transaction creating endpoints now accept `hardFee` for specifying the exact fee. diff --git a/lib/wallet/layout.js b/lib/wallet/layout.js index 98ea3cdab..43deba732 100644 --- a/lib/wallet/layout.js +++ b/lib/wallet/layout.js @@ -10,97 +10,159 @@ const bdb = require('bdb'); /* * Wallet Database Layout: + * WDB State + * --------- * V -> db version * O -> flags - * R -> chain sync state * D -> wallet id depth - * p[addr-hash] -> wallet ids - * P[wid][addr-hash] -> path data - * r[wid][index][hash] -> path account index + * M -> migration state + * + * Chain Sync + * ---------- + * R -> chain sync state + * h[height] -> block hash + * + * WID mappings + * -------- + * b[height] -> block->wid map + * T[tx-hash] -> tx->wid map + * o[tx-hash][index] -> outpoint->wid map + * p[addr-hash] -> address->wid map + * N[name-hash] -> name->wid map + * + * Wallet + * ------ + * l[id] -> wid * w[wid] -> wallet * W[wid] -> wallet id - * l[id] -> wid + * + * Wallet Account + * -------------- * a[wid][index] -> account * i[wid][name] -> account index * n[wid][index] -> account name - * h[height] -> recent block hash - * b[height] -> block->wid map - * o[hash][index] -> outpoint->wid map - * T[hash] -> tx->wid map + * + * Wallet Path + * ----------- + * P[wid][addr-hash] -> path data + * r[wid][index][addr-hash] -> dummy (addr by account) + * + * TXDB + * ---- * t[wid]* -> txdb - * N[hash256] -> name map - * M -> migration state */ exports.wdb = { + // WDB State V: bdb.key('V'), O: bdb.key('O'), - R: bdb.key('R'), D: bdb.key('D'), + M: bdb.key('M'), + + // Chain Sync + R: bdb.key('R'), + h: bdb.key('h', ['uint32']), + + // WID Mappings + b: bdb.key('b', ['uint32']), + T: bdb.key('T', ['hash256']), p: bdb.key('p', ['hash']), - P: bdb.key('P', ['uint32', 'hash']), - r: bdb.key('r', ['uint32', 'uint32', 'hash']), + o: bdb.key('o', ['hash256', 'uint32']), + N: bdb.key('N', ['hash256']), + + // Wallet + l: bdb.key('l', ['ascii']), w: bdb.key('w', ['uint32']), W: bdb.key('W', ['uint32']), - l: bdb.key('l', ['ascii']), + + // Wallet Account a: bdb.key('a', ['uint32', 'uint32']), i: bdb.key('i', ['uint32', 'ascii']), n: bdb.key('n', ['uint32', 'uint32']), - h: bdb.key('h', ['uint32']), - b: bdb.key('b', ['uint32']), - o: bdb.key('o', ['hash256', 'uint32']), - T: bdb.key('T', ['hash256']), - t: bdb.key('t', ['uint32']), - N: bdb.key('N', ['hash256']), - M: bdb.key('M') + + // Wallet Path + P: bdb.key('P', ['uint32', 'hash']), + r: bdb.key('r', ['uint32', 'uint32', 'hash']), + + // TXDB + t: bdb.key('t', ['uint32']) }; /* * TXDB Database Layout: + * Balance + * ------- * R -> wallet balance * r[account] -> account balance - * t[hash] -> extended tx - * c[hash][index] -> coin - * d[hash][index] -> undo coin - * s[hash][index] -> spent by hash - * p[hash] -> dummy (pending flag) - * m[time][hash] -> dummy (tx by time) - * h[height][hash] -> dummy (tx by height) - * T[account][hash] -> dummy (tx by account) - * P[account][hash] -> dummy (pending tx by account) - * M[account][time][hash] -> dummy (tx by time + account) - * H[account][height][hash] -> dummy (tx by height + account) - * C[account][hash][index] -> dummy (coin by account) + * + * Coin + * ---- + * c[tx-hash][index] -> coin + * C[account][tx-hash][index] -> dummy (coin by account) + * d[tx-hash][index] -> undo coin + * s[tx-hash][index] -> spent by hash + * + * Transaction + * ----------- + * 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) + * + * Confirmed + * --------- * b[height] -> block record + * h[height][tx-hash] -> dummy (tx by height) + * H[account][height][tx-hash] -> dummy (tx by height + account) + * + * Unconfirmed + * ----------- + * p[hash] -> dummy (pending tx) + * P[account][tx-hash] -> dummy (pending tx by account) + * + * Names + * ----- + * A[name-hash] -> name record (name record by name hash) + * U[tx-hash] -> name undo record (name undo record by tx hash) + * i[name-hash][tx-hash][index] -> bid (BlindBid by name + tx + index) + * B[name-hash][tx-hash][index] -> reveal (BidReveal by name + tx + index) + * v[blind-hash] -> blind (Blind Value by blind hash) + * o[name-hash] -> tx hash OPEN only (tx hash by name hash) */ exports.txdb = { prefix: bdb.key('t', ['uint32']), + + // Balance R: bdb.key('R'), r: bdb.key('r', ['uint32']), - t: bdb.key('t', ['hash256']), + + // Coin c: bdb.key('c', ['hash256', 'uint32']), + C: bdb.key('C', ['uint32', 'hash256', 'uint32']), d: bdb.key('d', ['hash256', 'uint32']), s: bdb.key('s', ['hash256', 'uint32']), - p: bdb.key('p', ['hash256']), - m: bdb.key('m', ['uint32', 'hash256']), - h: bdb.key('h', ['uint32', 'hash256']), + + // Transaction + t: bdb.key('t', ['hash256']), T: bdb.key('T', ['uint32', 'hash256']), - P: bdb.key('P', ['uint32', 'hash256']), + m: bdb.key('m', ['uint32', 'hash256']), M: bdb.key('M', ['uint32', 'uint32', 'hash256']), - H: bdb.key('H', ['uint32', 'uint32', 'hash256']), - C: bdb.key('C', ['uint32', 'hash256', 'uint32']), + + // Confirmed b: bdb.key('b', ['uint32']), - // Name records + h: bdb.key('h', ['uint32', 'hash256']), + H: bdb.key('H', ['uint32', 'uint32', 'hash256']), + + // Unconfirmed + p: bdb.key('p', ['hash256']), + P: bdb.key('P', ['uint32', 'hash256']), + + // Names A: bdb.key('A', ['hash256']), - // Name undo records U: bdb.key('U', ['hash256']), - // Bids i: bdb.key('i', ['hash256', 'hash256', 'uint32']), - // Reveals B: bdb.key('B', ['hash256', 'hash256', 'uint32']), - // Blinds v: bdb.key('v', ['hash256']), - // Opens o: bdb.key('o', ['hash256']) }; diff --git a/lib/wallet/migrations.js b/lib/wallet/migrations.js index 1964c50b3..26eef7f95 100644 --- a/lib/wallet/migrations.js +++ b/lib/wallet/migrations.js @@ -110,11 +110,7 @@ class MigrateChangeAddress extends AbstractMigration { */ async migrate(b, pending) { - const wids = await this.ldb.keys({ - gte: layout.W.min(), - lte: layout.W.max(), - parse: key => layout.W.decode(key)[0] - }); + const wids = await this.db.getWallets(); let total = 0; for (const wid of wids) { @@ -249,6 +245,50 @@ class MigrateAccountLookahead extends AbstractMigration { } } +class MigrateTXDBBalances extends AbstractMigration { + /** + * Create TXDB Balance migration object. + * @param {WalletMigratorOptions} options + * @constructor + */ + + constructor(options) { + super(options); + + this.options = options; + this.logger = options.logger.context('txdb-balance-migration'); + this.db = options.db; + this.ldb = options.ldb; + } + + /** + * We always migrate. + * @returns {Promise} + */ + + async check() { + return types.MIGRATE; + } + + /** + * Actual migration + * @param {Batch} b + * @param {WalletMigrationResult} pending + * @returns {Promise} + */ + + async migrate(b, pending) { + await this.db.recalculateBalances(); + } + + static info() { + return { + name: 'TXDB balance refresh', + description: 'Refresh balances for TXDB after txdb updates' + }; + } +} + /** * Wallet migration results. * @alias module:blockchain.WalletMigrationResult @@ -382,12 +422,14 @@ exports.WalletMigrationResult = WalletMigrationResult; exports.migrations = { 0: MigrateMigrations, 1: MigrateChangeAddress, - 2: MigrateAccountLookahead + 2: MigrateAccountLookahead, + 3: MigrateTXDBBalances }; // Expose migrations exports.MigrateChangeAddress = MigrateChangeAddress; exports.MigrateMigrations = MigrateMigrations; exports.MigrateAccountLookahead = MigrateAccountLookahead; +exports.MigrateTXDBBalances = MigrateTXDBBalances; module.exports = exports; diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 5bb6f5ee1..734ef7305 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -166,6 +166,17 @@ class TXDB { b.del(layout.d.encode(spender.hash, spender.index)); } + /** + * Spend credit by spender/input record. + * Add undo coin to the input record. + * @param {Credit} credit + * @param {Outpoint} spender + */ + + addUndoToInput(b, credit, spender) { + b.put(layout.d.encode(spender.hash, spender.index), credit.coin.encode()); + } + /** * Write input record. * @param {TX} tx @@ -205,8 +216,9 @@ class TXDB { /** * Update account balance. + * @param {Batch} b * @param {Number} acct - * @param {Balance} delta + * @param {BalanceDelta} delta */ async updateAccountBalance(b, acct, delta) { @@ -957,18 +969,7 @@ class TXDB { state.tx(path, 1); state.coin(path, -1); state.unconfirmed(path, -coin.value); - - // FINALIZE is a special case: locked coins _leave_ the wallet. - if (tx.output(i) && tx.covenant(i).isFinalize()) { - if (!block) { - state.ulocked(path, -tx.outputs[i].value); - } else { - state.clocked(path, -tx.outputs[i].value); - // This is the first time we've seen this tx and it is in a block - // (probably from a rescan). Update unconfirmed locked balance also. - state.ulocked(path, -tx.outputs[i].value); - } - } + this.unlockBalances(state, credit, path, -1); if (!block) { // If the tx is not mined, we do not @@ -988,6 +989,8 @@ class TXDB { // coin so it can be reconnected // later during a reorg. state.confirmed(path, -coin.value); + this.unlockBalances(state, credit, path, height); + await this.removeCredit(b, credit, path); view.addCoin(coin); @@ -1005,14 +1008,6 @@ class TXDB { if (!path) continue; - // If the first time we see a TX is in a block - // (i.e. during a rescan) update the "unconfirmed" locked balance - // before updating the "confirmed" locked balance. - if (height !== -1) - await this.lockBalances(b, state, tx, i, path, -1); - - await this.lockBalances(b, state, tx, i, path, height); - details.setOutput(i, path); const credit = Credit.fromTX(tx, i, height); @@ -1021,9 +1016,24 @@ class TXDB { state.tx(path, 1); state.coin(path, 1); state.unconfirmed(path, output.value); + this.lockBalances(state, credit, path, -1); - if (block) + if (block) { state.confirmed(path, output.value); + this.lockBalances(state, credit, path, height); + } + + const spender = await this.getSpent(hash, i); + + if (spender) { + credit.spent = true; + this.addUndoToInput(b, credit, spender); + + // TODO: emit 'missed credit' + state.coin(path, -1); + state.unconfirmed(path, -output.value); + this.unlockBalances(state, credit, path, -1); + } await this.saveCredit(b, credit, path); } @@ -1137,6 +1147,9 @@ class TXDB { if (!credits[i]) { await this.removeInput(b, tx, i); + // NOTE: This check has been moved to the outputs + // processing in insert(pending), insert(block) and confirm. + // But will still be here just in case. const credit = await this.getCredit(hash, index); if (!credit) @@ -1167,17 +1180,15 @@ class TXDB { if (resolved) { state.coin(path, -1); state.unconfirmed(path, -coin.value); + this.unlockBalances(state, credit, path, -1); } + state.confirmed(path, -coin.value); + this.unlockBalances(state, credit, path, height); + // We can now safely remove the credit // entirely, now that we know it's also // been removed on-chain. - state.confirmed(path, -coin.value); - - // FINALIZE is a special case: locked coins _leave_ the wallet. - if (tx.output(i) && tx.covenant(i).isFinalize()) - state.clocked(path, -tx.outputs[i].value); - await this.removeCredit(b, credit, path); view.addCoin(coin); @@ -1192,13 +1203,13 @@ class TXDB { if (!path) continue; - await this.lockBalances(b, state, tx, i, path, height); - details.setOutput(i, path); let credit = await this.getCredit(hash, i); if (!credit) { + // TODO: Emit 'missed credit' event. + // This credit didn't belong to us the first time we // saw the transaction (before confirmation or rescan). // Create new credit for database. @@ -1208,9 +1219,17 @@ class TXDB { // meaning if it becomes unconfirmed, we can still confidently spend it. credit.own = own; - // Add coin to "unconfirmed" balance (which includes confirmed coins) - state.coin(path, 1); - state.unconfirmed(path, credit.coin.value); + const spender = await this.getSpent(hash, i); + + if (spender) { + credit.spent = true; + this.addUndoToInput(b, credit, spender); + } else { + // Add coin to "unconfirmed" balance (which includes confirmed coins) + state.coin(path, 1); + state.unconfirmed(path, credit.coin.value); + this.lockBalances(state, credit, path, -1); + } } // Credits spent in the mempool add an @@ -1223,6 +1242,8 @@ class TXDB { // Update coin height and confirmed // balance. Save once again. state.confirmed(path, output.value); + this.lockBalances(state, credit, path, height); + credit.coin.height = height; await this.saveCredit(b, credit, path); @@ -1320,18 +1341,12 @@ class TXDB { state.tx(path, -1); state.coin(path, 1); state.unconfirmed(path, coin.value); + this.lockBalances(state, credit, path, -1); - // FINALIZE is a special case: locked coins _leave_ the wallet. - // In this case a TX is erased, adding them back. - if (tx.output(i) && tx.covenant(i).isFinalize()) { - if (!block) - state.ulocked(path, tx.outputs[i].value); - else - state.clocked(path, tx.outputs[i].value); - } - - if (block) + if (block) { state.confirmed(path, coin.value); + this.lockBalances(state, credit, path, height); + } this.unspendCredit(b, tx, i); @@ -1349,18 +1364,24 @@ class TXDB { if (!path) continue; - await this.unlockBalances(b, state, tx, i, path, height); + const credit = await this.getCredit(hash, i); - details.setOutput(i, path); + // If we don't have credit for the output, then we don't need + // to do anything, because they were getting erased anyway. + if (!credit) + continue; - const credit = Credit.fromTX(tx, i, height); + details.setOutput(i, path); state.tx(path, -1); state.coin(path, -1); state.unconfirmed(path, -output.value); + this.unlockBalances(state, credit, path, -1); - if (block) + if (block) { state.confirmed(path, -output.value); + this.unlockBalances(state, credit, path, height); + } await this.removeCredit(b, credit, path); } @@ -1515,6 +1536,7 @@ class TXDB { const {tx, hash, height} = wtx; const details = new Details(wtx, block); const state = new BalanceDelta(); + let own = false; assert(block); @@ -1544,15 +1566,12 @@ class TXDB { details.setInput(i, path, coin); state.confirmed(path, coin.value); - - // FINALIZE is a special case: locked coins _leave_ the wallet. - // In this case a TX is reversed, adding them back. - if (tx.output(i) && tx.covenant(i).isFinalize()) - state.clocked(path, tx.outputs[i].value); + this.lockBalances(state, credit, path, height); // Resave the credit and mark it // as spent in the mempool instead. credit.spent = true; + own = true; await this.saveCredit(b, credit, path); } } @@ -1566,18 +1585,40 @@ class TXDB { if (!path) continue; - await this.unlockBalances(b, state, tx, i, path, height); - - const credit = await this.getCredit(hash, i); + let credit = await this.getCredit(hash, i); + let resolved = false; // Potentially update undo coin height. if (!credit) { - await this.updateSpentCoin(b, tx, i, height); - continue; - } + // TODO: Emit 'missed credit' event. - if (credit.spent) - await this.updateSpentCoin(b, tx, i, height); + // This credit didn't belong to us the first time we + // saw the transaction (after confirmation). + // Create new credit for database. + credit = Credit.fromTX(tx, i, height); + + // If this tx spent any of our own coins, we "own" this output, + // meaning if it becomes unconfirmed, we can still confidently spend it. + credit.own = own; + resolved = true; + + const spender = await this.getSpent(hash, i); + + if (spender) { + credit.spent = true; + this.addUndoToInput(b, credit, spender); + } else { + // If the newly discovered Coin is not spent, + // we need to add these to the balance. + state.coin(path, 1); + state.unconfirmed(path, credit.coin.value); + this.lockBalances(state, credit, path, -1); + } + } else if (credit.spent) { + // The coin height of this output becomes -1 + // as it is being unconfirmed. + await this.updateSpentCoin(b, tx, i, -1); + } details.setOutput(i, path); @@ -1585,7 +1626,14 @@ class TXDB { // balance. Save once again. credit.coin.height = -1; - state.confirmed(path, -output.value); + // If the coin was not discovered now, it means + // we need to subtract the values as they were part of + // the balance. + // If the credit is new, confirmed balances did not account for it. + if (!resolved) { + state.confirmed(path, -output.value); + this.unlockBalances(state, credit, path, height); + } await this.saveCredit(b, credit, path); } @@ -1658,6 +1706,7 @@ class TXDB { * double spenders, and verify inputs. * @private * @param {TX} tx + * @param {Boolean} conf * @returns {Promise} */ @@ -1703,167 +1752,73 @@ class TXDB { } /** - * Lock balances according to covenants. - * @param {Object} b + * Lock balances according to covenant. + * Inserting or confirming: TX outputs. + * Removing or undoing: Coins spent by the wallet in tx inputs. * @param {State} state - * @param {TX} tx - * @param {Number} i + * @param {Credit} credit * @param {Path} path * @param {Number} height */ - async lockBalances(b, state, tx, i, path, height) { - const output = tx.outputs[i]; - const covenant = output.covenant; + lockBalances(state, credit, path, height) { + const {value, covenant} = credit.coin; switch (covenant.type) { - case types.CLAIM: - case types.BID: { + case types.CLAIM: // output is locked until REGISTER + case types.BID: // output is locked until REVEAL + case types.REVEAL: // output is locked until REDEEM + case types.REGISTER: // output is now locked or "burned" + case types.UPDATE: // output has been locked since REGISTER + case types.RENEW: + case types.TRANSFER: + case types.FINALIZE: + case types.REVOKE: + { if (height === -1) - state.ulocked(path, output.value); + state.ulocked(path, value); else - state.clocked(path, output.value); + state.clocked(path, value); break; } - case types.REVEAL: { - assert(i < tx.inputs.length); - - const nameHash = covenant.getHash(0); - const prevout = tx.inputs[i].prevout; - - const bb = await this.getBid(nameHash, prevout); - if (!bb) - break; - - if (height === -1) { - state.ulocked(path, -bb.lockup); - state.ulocked(path, output.value); - } else { - state.clocked(path, -bb.lockup); - state.clocked(path, output.value); - } - + case types.REDEEM: // noop: already unlocked by the BID in the input break; - } - - case types.REDEEM: { - if (height === -1) - state.ulocked(path, -output.value); - else - state.clocked(path, -output.value); - break; - } - - case types.REGISTER: { - assert(i < tx.inputs.length); - - const prevout = tx.inputs[i].prevout; - - const coin = await this.getCoin(prevout.hash, prevout.index); - assert(coin); - assert(coin.covenant.isReveal() || coin.covenant.isClaim()); - - if (height === -1) { - state.ulocked(path, -coin.value); - state.ulocked(path, output.value); - } else { - state.clocked(path, -coin.value); - state.clocked(path, output.value); - } - - break; - } - - case types.FINALIZE: { - if (height === -1) - state.ulocked(path, output.value); - else - state.clocked(path, output.value); - break; - } } } /** * Unlock balances according to covenants. - * @param {Object} b + * Inserting or confirming: Coins spent by the wallet in TX inputs. + * Removing or undoing: TX outputs. * @param {State} state - * @param {TX} tx - * @param {Number} i + * @param {Credit} credit * @param {Path} path * @param {Number} height */ - async unlockBalances(b, state, tx, i, path, height) { - const output = tx.outputs[i]; - const covenant = output.covenant; + unlockBalances(state, credit, path, height) { + const {value, covenant} = credit.coin; switch (covenant.type) { - case types.CLAIM: - case types.BID: { + case types.CLAIM: // output is locked until REGISTER + case types.BID: // output is locked until REVEAL + case types.REVEAL: // output is locked until REDEEM + case types.REGISTER: // output is now locked or "burned" + case types.UPDATE: // output has been locked since REGISTER + case types.RENEW: + case types.TRANSFER: + case types.FINALIZE: + case types.REVOKE: + { if (height === -1) - state.ulocked(path, -output.value); + state.ulocked(path, -value); else - state.clocked(path, -output.value); - break; - } - - case types.REVEAL: { - assert(i < tx.inputs.length); - - const nameHash = covenant.getHash(0); - const prevout = tx.inputs[i].prevout; - - const bb = await this.getBid(nameHash, prevout); - if (!bb) - break; - - if (height === -1) { - state.ulocked(path, bb.lockup); - state.ulocked(path, -output.value); - } else { - state.clocked(path, bb.lockup); - state.clocked(path, -output.value); - } - + state.clocked(path, -value); break; } - - case types.REDEEM: { - if (height === -1) - state.ulocked(path, output.value); - else - state.clocked(path, output.value); + case types.REDEEM: // noop: already unlocked by the BID in the input break; - } - - case types.REGISTER: { - assert(i < tx.inputs.length); - - const coins = await this.getSpentCoins(tx); - const coin = coins[i]; - assert(coin); - assert(coin.covenant.isReveal() || coin.covenant.isClaim()); - - if (height === -1) { - state.ulocked(path, coin.value); - state.ulocked(path, -output.value); - } else { - state.clocked(path, coin.value); - state.clocked(path, -output.value); - } - - break; - } - - case types.FINALIZE: { - if (height === -1) - state.ulocked(path, -output.value); - else - state.clocked(path, -output.value); - break; - } } } @@ -2271,6 +2226,65 @@ class TXDB { return true; } + /** + * Recalculate wallet balances. + * @returns {Promise} + */ + + async recalculateBalances() { + const state = new BalanceDelta(); + + const creditIter = this.bucket.iterator({ + gte: layout.c.min(), + lte: layout.c.max(), + values: true + }); + + await creditIter.each(async (key, raw) => { + const credit = Credit.decode(raw); + const coin = credit.coin; + const value = coin.value; + const path = await this.getPath(coin); + + assert(path); + + state.coin(path, 1); + state.unconfirmed(path, value); + this.lockBalances(state, credit, path, -1); + + // Unconfirmed coins + if (coin.height !== -1) { + state.confirmed(path, value); + this.lockBalances(state, credit, path, coin.height); + } + + if (credit.spent) { + state.coin(path, -1); + state.unconfirmed(path, -value); + this.unlockBalances(state, credit, path, -1); + } + }); + + const batch = this.bucket.batch(); + + for (const [acct, delta] of state.accounts) { + const oldAccountBalance = await this.getAccountBalance(acct); + const finalAcctBalance = new Balance(); + finalAcctBalance.tx = oldAccountBalance.tx; + + delta.applyTo(finalAcctBalance); + batch.put(layout.r.encode(acct), finalAcctBalance.encode()); + } + + const walletBalance = await this.getWalletBalance(); + const finalWalletBalance = new Balance(); + finalWalletBalance.tx = walletBalance.tx; + state.applyTo(finalWalletBalance); + batch.put(layout.R.encode(), finalWalletBalance.encode()); + + await batch.write(); + } + /** * Lock all coins in a transaction. * @param {TX} tx @@ -4026,4 +4040,14 @@ class BidReveal extends bio.Struct { * Expose */ +TXDB.Balance = Balance; +TXDB.BalanceDelta = BalanceDelta; +TXDB.Credit = Credit; +TXDB.Details = Details; +TXDB.DetailsMember = DetailsMember; +TXDB.BlockRecord = BlockRecord; +TXDB.BlindBid = BlindBid; +TXDB.BlindValue = BlindValue; +TXDB.BidReveal = BidReveal; + module.exports = TXDB; diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 16d8cc53d..c094cca1b 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -4822,6 +4822,23 @@ class Wallet extends EventEmitter { } } + /** + * Recalculate balances + * @returns {Promise} + */ + + async recalculateBalances() { + const unlock1 = await this.writeLock.lock(); + const unlock2 = await this.fundLock.lock(); + + try { + return await this.txdb.recalculateBalances(); + } finally { + unlock2(); + unlock1(); + } + } + /** * Zap stale TXs from wallet. * @param {(Number|String)?} acct diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 8f809d6bd..7a8c25277 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -533,22 +533,18 @@ class WalletDB extends EventEmitter { lte: layout.T.max() }); - const wids = await this.db.keys({ - gte: layout.W.min(), - lte: layout.W.max(), - parse: key => layout.W.decode(key)[0] - }); + const wnames = await this.getWallets(); - for (const wid of wids) { - const wallet = await this.get(wid); + for (const wname of wnames) { + const wallet = await this.get(wname); this.logger.warning( 'Clearing all tx history for wallet: %s (%d)', - wallet.id, wid + wallet.id, wallet.wid ); // remove all txdb data *except* blinds ('v') const key = 'v'.charCodeAt(); - const prefix = layout.t.encode(wid); + const prefix = layout.t.encode(wallet.wid); await removeRange({ gte: Buffer.concat([prefix, Buffer.alloc(1)]), lt: Buffer.concat([prefix, Buffer.from([key])]) @@ -590,6 +586,35 @@ class WalletDB extends EventEmitter { return this.scan(height); } + /** + * Recalculate balances from the coins. + * @returns {Promise} + */ + + async recalculateBalances() { + const unlock = await this.txLock.lock(); + + try { + return await this._recalculateBalances(); + } finally { + unlock(); + } + } + + /** + * Recalculate balances from the coins (without a lock). + * @returns {Promise} + */ + + async _recalculateBalances() { + const wnames = await this.getWallets(); + + for (const wname of wnames) { + const wallet = await this.get(wname); + await wallet.recalculateBalances(); + } + } + /** * Broadcast a transaction via chain server. * @param {TX} tx @@ -1547,8 +1572,8 @@ class WalletDB extends EventEmitter { } /** - * Get all wallet ids. - * @returns {Promise} + * Get all wallet names. + * @returns {Promise} */ async getWallets() { diff --git a/test/wallet-balance-test.js b/test/wallet-balance-test.js new file mode 100644 index 000000000..403bbcd8d --- /dev/null +++ b/test/wallet-balance-test.js @@ -0,0 +1,2615 @@ +'use strict'; + +const assert = require('bsert'); +const Network = require('../lib/protocol/network'); +const Mnemonic = require('../lib/hd/mnemonic'); +const FullNode = require('../lib/node/fullnode'); +const WalletPlugin = require('../lib/wallet/plugin'); +const MTX = require('../lib/primitives/mtx'); +const Coin = require('../lib/primitives/coin'); +const Output = require('../lib/primitives/output'); +const {Resource} = require('../lib/dns/resource'); +const {types, grindName} = require('../lib/covenants/rules'); +const {forEventCondition} = require('./util/common'); + +/** + * Wallet balance tracking tests. + * + * TODO: + * - Add CoinView support to chain <-> wallet and update input tests. + * - Add coin discovery on unconfirm + * - Add spent coin state recovery on unconfirm/confirm for pending txs. + * - Add spent coin state recovery on insert/insert(block) and confirm. + */ + +const network = Network.get('regtest'); +const mnemData = require('./data/mnemonic-english.json'); + +// make wallets addrs deterministic. +const phrases = mnemData.map(d => Mnemonic.fromPhrase(d[1])); + +const { + treeInterval, + biddingPeriod, + revealPeriod, + transferLockup +} = network.names; + +/** + * @enum {Number} + */ + +const DISCOVER_TYPES = { + NONE: 0, + BEFORE_CONFIRM: 1, + BEFORE_UNCONFIRM: 2, + BEFORE_ERASE: 3, + BEFORE_BLOCK_CONFIRM: 4, + BEFORE_BLOCK_UNCONFIRM: 5 +}; + +const { + NONE, + BEFORE_CONFIRM, + BEFORE_UNCONFIRM, + BEFORE_ERASE, + BEFORE_BLOCK_CONFIRM, + BEFORE_BLOCK_UNCONFIRM +} = DISCOVER_TYPES; + +const openingPeriod = treeInterval + 2; + +// default gen wallets. +const WALLET_N = 5; +const GRIND_NAME_LEN = 10; + +// Wallet consts +const DEFAULT_ACCOUNT = 'default'; +const ALT_ACCOUNT = 'alt'; + +// Balances +/** + * @property {Number} tx + * @property {Number} coin + * @property {Number} confirmed + * @property {Number} unconfirmed + * @property {Number} ulocked - unconfirmed locked + * @property {Number} clocked - confirmed locked + */ + +class BalanceObj { + constructor(options) { + options = options || {}; + + this.tx = options.tx || 0; + this.coin = options.coin || 0; + this.confirmed = options.confirmed || 0; + this.unconfirmed = options.unconfirmed || 0; + this.ulocked = options.ulocked || 0; + this.clocked = options.clocked || 0; + } + + clone() { + return new BalanceObj(this); + } + + cloneWithDelta(obj) { + return this.clone().apply(obj); + } + + fromBalance(obj) { + this.tx = obj.tx; + this.coin = obj.coin; + this.confirmed = obj.confirmed; + this.unconfirmed = obj.unconfirmed; + this.ulocked = obj.lockedUnconfirmed; + this.clocked = obj.lockedConfirmed; + + return this; + } + + apply(balance) { + this.tx += balance.tx || 0; + this.coin += balance.coin || 0; + this.confirmed += balance.confirmed || 0; + this.unconfirmed += balance.unconfirmed || 0; + this.ulocked += balance.ulocked || 0; + this.clocked += balance.clocked || 0; + + return this; + } + + static fromBalance(wbalance) { + return new this().fromBalance(wbalance); + } +} + +const INIT_BLOCKS = treeInterval; +const INIT_FUND = 10e6; +const NULL_BALANCE = new BalanceObj({ + tx: 0, + coin: 0, + unconfirmed: 0, + confirmed: 0, + ulocked: 0, + clocked: 0 +}); + +const INIT_BALANCE = new BalanceObj({ + tx: 1, + coin: 1, + unconfirmed: INIT_FUND, + confirmed: INIT_FUND, + ulocked: 0, + clocked: 0 +}); + +const HARD_FEE = 1e4; +const SEND_AMOUNT = 2e6; +const SEND_AMOUNT_2 = 3e6; + +// first is loser if it matters. +const BLIND_AMOUNT_1 = 1e6; +const BID_AMOUNT_1 = BLIND_AMOUNT_1 / 4; +const BLIND_ONLY_1 = BLIND_AMOUNT_1 - BID_AMOUNT_1; + +// second is winner. +const BLIND_AMOUNT_2 = 2e6; +const BID_AMOUNT_2 = BLIND_AMOUNT_2 / 4; +const BLIND_ONLY_2 = BLIND_AMOUNT_2 - BID_AMOUNT_2; + +// Loser balances for primary +const FINAL_PRICE_1 = 1e5; +const FINAL_PRICE_2 = 2e5; // less then 1e6/4 (2.5e5) + +// const BLIND_AMOUNT_3 = 3e6; +// const BID_AMOUNT_3 = BLIND_AMOUNT_3 / 4; +// const BLIND_ONLY_3 = BLIND_AMOUNT_3 - BID_AMOUNT_3; + +// Empty resource +const EMPTY_RS = Resource.fromJSON({ records: [] }); + +/* + * Wallet helpers + */ + +async function getAddrStr(wallet, acct = 0) { + return (await wallet.receiveAddress(acct)).toString(network); +} + +function getAheadAddr(account, ahead, master) { + const nextIndex = account.receiveDepth + account.lookahead + ahead; + const receiveKey = account.deriveReceive(nextIndex, master); + const nextAddr = receiveKey.getAddress(); + + return { nextAddr, receiveKey }; +} + +async function catchUpToAhead(wallet, accountName, ahead) { + for (let i = 0; i < ahead; i++) + await wallet.createReceive(accountName); +}; + +async function resign(wallet, mtx) { + for (const input of mtx.inputs) + input.witness.length = 0; + + await wallet.sign(mtx); +}; + +/* + * Balance helpers + */ + +/** + * @returns {Promise} + */ + +async function getBalanceObj(wallet, accountName) { + const balance = await wallet.getBalance(accountName); + return BalanceObj.fromBalance(balance.getJSON(true)); +} + +async function assertBalance(wallet, accountName, expected, message) { + const balance = await getBalanceObj(wallet, accountName); + assert.deepStrictEqual(balance, expected, message); + + // recalculate balance test + await wallet.recalculateBalances(); + const balance2 = await getBalanceObj(wallet, accountName); + assert.deepStrictEqual(balance2, expected, message); +} + +async function assertRecalcBalance(wallet, accountName, expected, message) { + await wallet.recalculateBalances(); + const balance = await getBalanceObj(wallet, accountName); + assert.deepStrictEqual(balance, expected, message); +} + +/** + * @param {BalanceObj} balance + * @param {BalanceObj} delta + * @returns {BalanceObj} + */ + +function applyDelta(balance, delta) { + return balance.clone().apply(delta); +} + +describe('Wallet Balance', function() { + let node, chain, wdb, genWallets = WALLET_N;; + + // wallets + // alt wallets are clones of allWallets to aid us in lookahead test. + let primary, walletIndex, allWallets = [], cloneWallets = []; + + /* + * Contextual helpers + */ + + const prepare = () => { + node = new FullNode({ + network: network.type, + memory: true, + plugins: [WalletPlugin], + noDNS: true, + noNS: true + }); + + chain = node.chain; + wdb = node.require('walletdb').wdb; + }; + + const mineBlocks = async (blocks) => { + const tipHeight = chain.tip.height; + const forWalletBlock = forEventCondition(wdb, 'block connect', (entry) => { + return entry.height === tipHeight + 1; + }); + await node.rpc.generateToAddress([blocks, await getAddrStr(primary)]); + await forWalletBlock; + }; + + const setupWallets = async () => { + walletIndex = 0; + primary = await wdb.get('primary'); + + allWallets = []; + cloneWallets = []; + for (let i = 0; i < genWallets; i++) { + const name = 'wallet' + i; + const wallet = await wdb.create({ id: name, mnemonic: phrases[i] }); + const clone = await wdb.create({ id: name + '-alt', mnemonic: phrases[i] }); + allWallets.push(wallet); + cloneWallets.push(clone); + } + }; + + const fundWallets = async () => { + await mineBlocks(INIT_BLOCKS); + const addrs = []; + + for (let i = 0; i < genWallets; i++) + addrs.push(await getAddrStr(allWallets[i], DEFAULT_ACCOUNT)); + + await primary.send({ + outputs: addrs.map((addr) => { + return { + value: INIT_FUND, + address: addr + }; + }) + }); + await mineBlocks(1); + }; + + const getNextWallet = (index) => { + const i = index ? index : walletIndex++; + + if (!allWallets[i]) + throw new Error('There are not enough wallets, can not get at index: ' + i); + + return { + index: i, + wallet: allWallets[i], + wid: allWallets[i].id, + clone: cloneWallets[i], + cloneWID: cloneWallets[i].id, + opts: { + account: DEFAULT_ACCOUNT, + hardFee: HARD_FEE + } + }; + }; + + const forWTX = (id, hash) => { + return forEventCondition(wdb, 'tx', (wallet, tx) => { + return wallet.id === id && tx.hash().equals(hash); + }); + }; + + /* + * beforeall and afterall for each describe + */ + + const beforeAll = async () => { + prepare(); + + await node.open(); + await setupWallets(); + await fundWallets(); + }; + + const afterAll = async () => { + await node.close(); + node = null; + + // reduce time of the tests. + if (walletIndex !== genWallets) + console.log(`Leftover wallets, used: ${walletIndex} of ${genWallets}.`); + + genWallets = WALLET_N; + }; + + /* + * Balance testing steps. + */ + + /** + * @callback BalanceCheckFunction + * @param {Wallet} wallet + * @param {Wallet} clone + * @param {Number} ahead + * @param {Object} [opts] + */ + + /** + * @typedef {Object} TestBalances + * @property {BalanceObj} TestBalances.initialBalance + * @property {BalanceObj} TestBalances.sentBalance + * @property {BalanceObj} TestBalances.confirmedBalance + * @property {BalanceObj} TestBalances.unconfirmedBalance + * @property {BalanceObj} TestBalances.eraseBalance + * @property {BalanceObj} TestBalances.blockConfirmedBalance + * @property {BalanceObj} TestBalances.blockUnconfirmedBalance + * @property {BalanceObj} [TestBalances.blockFinalConfirmedBalance] + */ + + /** + * @typedef {Object} CheckFunctions + * @property {BalanceCheckFunction} CheckFunctions.initCheck + * @property {BalanceCheckFunction} CheckFunctions.sentCheck + * @property {BalanceCheckFunction} CheckFunctions.confirmedCheck + * @property {BalanceCheckFunction} CheckFunctions.unconfirmedCheck + * @property {BalanceCheckFunction} CheckFunctions.eraseCheck + * @property {BalanceCheckFunction} CheckFunctions.blockConfirmCheck + * @property {BalanceCheckFunction} CheckFunctions.blockUnconfirmCheck + * @property {BalanceCheckFunction} CheckFunctions.blockFinalConfirmCheck + */ + + /** + * @callback BalanceTestFunction + * @param {CheckFunctions} checks + * @param {BalanceCheckFunction} discoverFn + * @param {DISCOVER_TYPES} discoverAt + * @param {Object} opts + */ + + /** + * Supports missing address/discoveries at certain points. + * @param {BalanceCheckFunction} [setupFn] + * @param {BalanceCheckFunction} receiveFn + * @param {Number} ahead + * @returns {BalanceTestFunction} + */ + + const balanceTest = (setupFn, receiveFn, ahead) => { + return async (checks, discoverFn, discoverAt, opts = {}) => { + const {wallet, clone} = getNextWallet(); + + if (setupFn) + await setupFn(wallet, clone, ahead, opts); + + await checks.initCheck(wallet, ahead, opts); + + await receiveFn(wallet, clone, ahead, opts); + await checks.sentCheck(wallet, ahead, opts); + + if (discoverAt === BEFORE_CONFIRM) + await discoverFn(wallet, ahead, opts); + + await mineBlocks(1); + await checks.confirmedCheck(wallet, ahead, opts); + + // now unconfirm + if (discoverAt === BEFORE_UNCONFIRM) + await discoverFn(wallet, ahead, opts); + + await wdb.revert(chain.tip.height - 1); + await checks.unconfirmedCheck(wallet, ahead, opts); + + // now erase + if (discoverAt === BEFORE_ERASE) + await discoverFn(wallet, ahead, opts); + + await wallet.zap(-1, 0); + await checks.eraseCheck(wallet, ahead, opts); + + if (discoverAt === BEFORE_BLOCK_CONFIRM) + await discoverFn(wallet, ahead, opts); + + // Final look at full picture. + await wdb.scan(chain.tip.height - 1); + await checks.blockConfirmCheck(wallet, ahead, opts); + + if (discoverAt === BEFORE_BLOCK_UNCONFIRM) + await discoverFn(wallet, ahead, opts); + + // Unconfirm + await wdb.revert(chain.tip.height - 1); + await checks.blockUnconfirmCheck(wallet, ahead, opts); + + // Clean up wallet. + await wdb.scan(chain.tip.height - 1); + await checks.blockFinalConfirmCheck(wallet, ahead, opts); + }; + }; + + const BALANCE_CHECK_MAP = { + initCheck: ['initialBalance', 'Initial'], + sentCheck: ['sentBalance', 'Sent'], + confirmedCheck: ['confirmedBalance', 'Confirmed'], + unconfirmedCheck: ['unconfirmedBalance', 'Unconfirmed'], + eraseCheck: ['eraseBalance', 'Erase'], + blockConfirmCheck: ['blockConfirmedBalance', 'Block confirmed'], + blockUnconfirmCheck: ['blockUnconfirmedBalance', 'Block unconfirmed'], + blockFinalConfirmCheck: ['blockFinalConfirmedBalance', 'Block final confirmed'] + }; + + /** + * Check also wallet, default and alt account balances. + * @param {TestBalances} walletBalances + * @param {TestBalances} [defBalances] - default account + * @param {TestBalances} [altBalances] - alt account balances + * @returns {CheckFunctions} + */ + + const checkBalances = (walletBalances, defBalances, altBalances) => { + const checks = {}; + + if (defBalances == null) + defBalances = walletBalances; + + for (const [key, [balanceName, name]] of Object.entries(BALANCE_CHECK_MAP)) { + checks[key] = async (wallet) => { + await assertBalance( + wallet, + DEFAULT_ACCOUNT, + defBalances[balanceName], + `${name} balance is incorrect in the account ${DEFAULT_ACCOUNT}.` + ); + + await assertRecalcBalance( + wallet, + DEFAULT_ACCOUNT, + defBalances[balanceName], + `${name} balance is incorrect ` + + `after recalculation in the account ${DEFAULT_ACCOUNT}.` + ); + + if (altBalances != null) { + await assertBalance( + wallet, + ALT_ACCOUNT, + altBalances[balanceName], + `${name} balance is incorrect in the account ${ALT_ACCOUNT}.` + ); + + await assertRecalcBalance( + wallet, + ALT_ACCOUNT, + altBalances[balanceName], + `${name} balance is incorrect ` + + `after recalculation in the account ${ALT_ACCOUNT}.` + ); + } + + await assertBalance( + wallet, + -1, + walletBalances[balanceName], + `${name} balance is incorrect for the wallet.` + ); + + await assertRecalcBalance( + wallet, + -1, + walletBalances[balanceName], + `${name} balance is incorrect ` + + 'after recalculate for the wallet.' + ); + }; + } + + return checks; + }; + + const combineBalances = (undiscovered, discovered, discoverAt) => { + if (Array.isArray(undiscovered) && Array.isArray(discovered)) { + const combined = []; + + for (let i = 0; i < undiscovered.length; i++) + combined.push(combineBalances(undiscovered[i], discovered[i], discoverAt)); + + return combined; + } + + const balances = { ...undiscovered }; + + if (balances.blockFinalConfirmedBalance == null) + balances.blockFinalConfirmedBalance = balances.blockConfirmedBalance; + + switch (discoverAt) { + case BEFORE_CONFIRM: + balances.confirmedBalance = discovered.confirmedBalance; + case BEFORE_UNCONFIRM: + balances.unconfirmedBalance = discovered.unconfirmedBalance; + case BEFORE_ERASE: + case BEFORE_BLOCK_CONFIRM: + balances.blockConfirmedBalance = discovered.blockConfirmedBalance; + case BEFORE_BLOCK_UNCONFIRM: + balances.blockUnconfirmedBalance = discovered.blockUnconfirmedBalance; + balances.blockFinalConfirmedBalance = discovered.blockConfirmedBalance; + case NONE: + default: + } + + return balances; + }; + + const defDiscover = async (wallet, ahead) => { + await catchUpToAhead(wallet, DEFAULT_ACCOUNT, ahead); + }; + + const altDiscover = async (wallet, ahead) => { + await catchUpToAhead(wallet, ALT_ACCOUNT, ahead); + }; + + const genTests = (options) => { + const { + name, + undiscovered, + discovered, + tester, + discoverer + } = options; + + const genTestBody = (type) => { + // three balances including alt are different. + if (Array.isArray(undiscovered)) { + return async () => { + const balances = combineBalances(undiscovered, discovered, type); + await tester(checkBalances(balances[0], balances[1], balances[2]), discoverer, type); + }; + } + return async () => { + const balances = combineBalances(undiscovered, discovered, type); + await tester(checkBalances(balances), discoverer, type); + }; + }; + + it(`${name} (no discovery)`, genTestBody(NONE)); + it(`${name}, discover on confirm`, genTestBody(BEFORE_CONFIRM)); + it(`${name}, discover on unconfirm`, genTestBody(BEFORE_UNCONFIRM)); + it(`${name}, discover on erase`, genTestBody(BEFORE_ERASE)); + it(`${name}, discover on block confirm`, genTestBody(BEFORE_CONFIRM)); + it(`${name}, discover on block unconfirm`, genTestBody(BEFORE_ERASE)); + }; + + /** + * One to normal address. + * One in the future address + * These functions accumulate: + * fees: 1 + 1 + 1? + * coins: 2 + 1 + 1? + * tx: 1 + 1 + 1? + * + * Missing 1 register. + */ + + const INIT_REGISTERED_BALANCE = applyDelta(INIT_BALANCE, { + tx: 3, + coin: 3, + + // missing second FINAL_PRICE_2 register. + confirmed: -(HARD_FEE * 3) - FINAL_PRICE_2, + unconfirmed: -(HARD_FEE * 3) - FINAL_PRICE_2, + + clocked: FINAL_PRICE_1, + ulocked: FINAL_PRICE_1 + }); + + const setupTwoRegisteredNames = async (wallet, ahead, register = true) => { + const name1 = grindName(GRIND_NAME_LEN, chain.tip.height, network); + const name2 = grindName(GRIND_NAME_LEN, chain.tip.height, network); + + const account = await wallet.getAccount(DEFAULT_ACCOUNT); + const {nextAddr} = getAheadAddr(account, ahead); + + await primary.sendBatch([ + ['OPEN', name1], + ['OPEN', name2] + ]); + await mineBlocks(openingPeriod); + + const txOpts = { hardFee: HARD_FEE }; + + // all three bids are there. + const bidMTX = await wallet.createBatch([ + ['BID', name1, BID_AMOUNT_1, BLIND_AMOUNT_1], + ['BID', name2, BID_AMOUNT_2, BLIND_AMOUNT_2] + ], txOpts); + + assert.strictEqual(bidMTX.outputs[0].covenant.type, types.BID); + assert.strictEqual(bidMTX.outputs[1].covenant.type, types.BID); + + bidMTX.outputs[1].address = nextAddr; + await resign(wallet, bidMTX); + + // make sure clone knows ahead addrs. + await defDiscover(wallet, ahead * 2); + + await node.mempool.addTX(bidMTX.toTX()); + + await mineBlocks(1); + + assert(FINAL_PRICE_1 <= BID_AMOUNT_1); + assert(FINAL_PRICE_2 <= BID_AMOUNT_2); + + // primary will lose + await primary.sendBid(name1, FINAL_PRICE_1, INIT_FUND); + await primary.sendBid(name2, FINAL_PRICE_2, INIT_FUND); + + await mineBlocks(biddingPeriod - 1); + + await primary.sendReveal(name1); + await primary.sendReveal(name2); + + await wallet.sendBatch([ + ['REVEAL', name1], + ['REVEAL', name2] + ], txOpts); + + await mineBlocks(revealPeriod); + + if (register !== false) { + await wallet.sendBatch([ + ['UPDATE', name1, EMPTY_RS], + ['UPDATE', name2, EMPTY_RS] + ], { + hardFee: HARD_FEE + }); + + await mineBlocks(1); + } + + return [name1, name2]; + }; + + describe('NONE -> NONE* (normal receive)', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + const receive = async (wallet, clone, ahead) => { + const recvAddr = await wallet.receiveAddress(); + const account = await wallet.getAccount(DEFAULT_ACCOUNT); + const {nextAddr} = getAheadAddr(account, ahead); + + // Send one to the normal address + // Send another one to the gapped/missed adress + await primary.send({ + outputs: [{ + address: recvAddr, + value: SEND_AMOUNT + }, { + address: nextAddr, + value: SEND_AMOUNT_2 + }] + }); + }; + + // account.lookahead + AHEAD + const AHEAD = 10; + const testReceive = balanceTest(null, receive, AHEAD); + + // Balances if we did not discover + const UNDISCOVERED = {}; + UNDISCOVERED.initialBalance = INIT_BALANCE; + UNDISCOVERED.sentBalance = applyDelta(UNDISCOVERED.initialBalance, { + tx: 1, + coin: 1, + unconfirmed: SEND_AMOUNT + }); + + UNDISCOVERED.confirmedBalance = applyDelta(UNDISCOVERED.sentBalance, { + confirmed: SEND_AMOUNT + }); + + UNDISCOVERED.unconfirmedBalance = UNDISCOVERED.sentBalance; + UNDISCOVERED.eraseBalance = UNDISCOVERED.initialBalance; + UNDISCOVERED.blockConfirmedBalance = UNDISCOVERED.confirmedBalance; + UNDISCOVERED.blockUnconfirmedBalance = UNDISCOVERED.sentBalance; + + // Balances if we discovered from the beginning + const DISCOVERED = {}; + DISCOVERED.initialBalance = UNDISCOVERED.initialBalance; + DISCOVERED.sentBalance = applyDelta(DISCOVERED.initialBalance, { + tx: 1, + coin: 2, + unconfirmed: SEND_AMOUNT + SEND_AMOUNT_2 + }); + + DISCOVERED.confirmedBalance = applyDelta(DISCOVERED.sentBalance, { + confirmed: SEND_AMOUNT + SEND_AMOUNT_2 + }); + + DISCOVERED.unconfirmedBalance = DISCOVERED.sentBalance; + DISCOVERED.eraseBalance = DISCOVERED.initialBalance; + DISCOVERED.blockConfirmedBalance = DISCOVERED.confirmedBalance; + DISCOVERED.blockUnconfirmedBalance = DISCOVERED.sentBalance; + + genTests({ + name: 'should handle normal receive', + undiscovered: UNDISCOVERED, + discovered: DISCOVERED, + tester: testReceive, + discoverer: defDiscover + }); + }); + + describe('NONE* -> NONE (spend our credits)', function() { + this.timeout(5000); + before(() => { + genWallets = 1; + return beforeAll(); + }); + + after(afterAll); + + let coins, nextAddr, receiveKey; + + const setupTXFromFuture = async (wallet, clone, ahead) => { + const recvAddr = await wallet.receiveAddress(); + const account = await wallet.getAccount(DEFAULT_ACCOUNT); + const aheadAddr = getAheadAddr(account, ahead, wallet.master); + nextAddr = aheadAddr.nextAddr; + receiveKey = aheadAddr.receiveKey; + + // Create transaction that creates two coins: + // 1. normal coin + // 2. one gapped/missed coin + const fundTX = await primary.send({ + sort: false, + outputs: [{ + address: recvAddr, + value: SEND_AMOUNT + }, { + address: nextAddr, + value: SEND_AMOUNT + HARD_FEE + }] + }); + + await mineBlocks(1); + + coins = [ + Coin.fromTX(fundTX, 0, chain.tip.height), + Coin.fromTX(fundTX, 1, chain.tip.height) + ]; + }; + + const receive = async (wallet) => { + const outAddr = await primary.receiveAddress(); + const changeAddr = await wallet.changeAddress(); + + // spend both coins in one tx. + const mtx = new MTX(); + + mtx.addOutput(new Output({ + address: outAddr, + value: SEND_AMOUNT * 2 + })); + + // HARD_FEE is paid by gapped/missed coin. + await mtx.fund(coins, { + hardFee: HARD_FEE, + changeAddress: changeAddr + }); + + await wallet.sign(mtx); + await mtx.signAsync(receiveKey); + + node.mempool.addTX(mtx.toTX()); + await forWTX(wallet.id, mtx.hash()); + }; + + const AHEAD = 10; + const test = balanceTest(setupTXFromFuture, receive, AHEAD); + + const UNDISCOVERED = {}; + UNDISCOVERED.initialBalance = applyDelta(INIT_BALANCE, { + tx: 1, + coin: 1, + confirmed: SEND_AMOUNT, + unconfirmed: SEND_AMOUNT + }); + + UNDISCOVERED.sentBalance = applyDelta(UNDISCOVERED.initialBalance, { + tx: 1, + coin: -1, + unconfirmed: -SEND_AMOUNT + }); + + UNDISCOVERED.confirmedBalance = applyDelta(UNDISCOVERED.sentBalance, { + confirmed: -SEND_AMOUNT + }); + + UNDISCOVERED.unconfirmedBalance = applyDelta(UNDISCOVERED.confirmedBalance, { + confirmed: SEND_AMOUNT + }); + + UNDISCOVERED.eraseBalance = applyDelta(UNDISCOVERED.unconfirmedBalance, { + tx: -1, + coin: 1, + unconfirmed: SEND_AMOUNT + }); + + UNDISCOVERED.blockConfirmedBalance = applyDelta(UNDISCOVERED.initialBalance, { + tx: 1, + coin: -1, + confirmed: -SEND_AMOUNT, + unconfirmed: -SEND_AMOUNT + }); + + UNDISCOVERED.blockUnconfirmedBalance = applyDelta(UNDISCOVERED.blockConfirmedBalance, { + confirmed: SEND_AMOUNT + }); + + UNDISCOVERED.blockFinalConfirmedBalance = UNDISCOVERED.blockConfirmedBalance; + + it('should spend normal credit (no discovery)', async () => { + const balances = UNDISCOVERED; + + await test( + checkBalances(balances), + defDiscover, + DISCOVER_TYPES.NONE + ); + }); + + it.skip('should spend credit, discover before confirm', async () => { + // TODO: Implement with coinview update. + // This will be no different than normal credit spend if + // we don't receive CoinView from the chain. So skip this until we + // have that feature. + }); + + // We don't have any details about inputs, so it's not possible to recover them. + // it('should spend credit, discover before unconfirm', async () => {}); + // it('should spend credit, discover before erase', async () => {}); + + it.skip('should spend credit, discover before block confirm', async () => { + // This will be no different than normal credit spend if + // we don't receive CoinView from the chain. So skip this until we + // have that feature. + }); + + // We don't have any details about inputs, so it's not possible to recover them. + // it('should spend credit, discover on block unconfirm', async () => { }); + }); + + describe('NONE* -> NONE* (receive and spend in pending)', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + const receive = async (wallet, clone, ahead) => { + const recvAddr = await wallet.receiveAddress(); + const account = await wallet.getAccount(DEFAULT_ACCOUNT); + const aheadAddr = getAheadAddr(account, ahead, wallet.master); + const nextAddr = aheadAddr.nextAddr; + const receiveKey = aheadAddr.receiveKey; + + // Create transaction that creates two coins: + // 1. normal coin + // 2. one gapped/missed coin + const fundTX = await primary.send({ + sort: false, + outputs: [{ + address: recvAddr, + value: SEND_AMOUNT + }, { + address: nextAddr, + value: SEND_AMOUNT + HARD_FEE + }] + }); + + const coins = [ + Coin.fromTX(fundTX, 0, chain.tip.height), + Coin.fromTX(fundTX, 1, chain.tip.height) + ]; + + const outAddr = await primary.receiveAddress(); + const changeAddr = await wallet.changeAddress(); + + // spend both coins in one tx. + const mtx = new MTX(); + + mtx.addOutput(new Output({ + address: outAddr, + value: SEND_AMOUNT * 2 + })); + + // HARD_FEE is paid by gapped/missed coin. + await mtx.fund(coins, { + hardFee: HARD_FEE, + changeAddress: changeAddr + }); + + await wallet.sign(mtx); + await mtx.signAsync(receiveKey); + + node.mempool.addTX(mtx.toTX()); + await forWTX(wallet.id, mtx.hash()); + }; + + const AHEAD = 10; + const test = balanceTest(null, receive, AHEAD); + + // Balances. + const UNDISCOVERED = {}; + + // For this test, the balances are same for all the test cases, + // but for different reasons. + UNDISCOVERED.initialBalance = INIT_BALANCE; + + // We receive 2 transactions (receiving one and spending one) + // But we spend discovered output right away. + UNDISCOVERED.sentBalance = applyDelta(UNDISCOVERED.initialBalance, { + tx: 2, + coin: 0, + unconfirmed: 0 + }); + + // Nothing changes for confirmed either. (Coins are spent in pending) + UNDISCOVERED.confirmedBalance = UNDISCOVERED.sentBalance; + UNDISCOVERED.unconfirmedBalance = UNDISCOVERED.confirmedBalance; + + // We no longer have two txs. + UNDISCOVERED.eraseBalance = applyDelta(UNDISCOVERED.unconfirmedBalance, { tx: -2 }); + UNDISCOVERED.blockConfirmedBalance = UNDISCOVERED.confirmedBalance; + UNDISCOVERED.blockUnconfirmedBalance = UNDISCOVERED.unconfirmedBalance; + + genTests({ + name: 'should spend credit', + undiscovered: UNDISCOVERED, + discovered: UNDISCOVERED, + tester: test, + discoverer: defDiscover + }); + }); + + describe('NONE -> OPEN', function() { + this.timeout(5000); + before(() => { + genWallets = 1; + return beforeAll(); + }); + + after(afterAll); + + const sendOpen = async (wallet) => { + const name = grindName(GRIND_NAME_LEN, chain.tip.height, network); + + await wallet.sendOpen(name, { + hardFee: HARD_FEE + }); + }; + + const testOpen = balanceTest(null, sendOpen, 0); + + it('should handle open', async () => { + const balances = {}; + balances.initialBalance = INIT_BALANCE; + + // TODO: Should 0 value outs be counted towards coin and stored in coin set? + balances.sentBalance = applyDelta(balances.initialBalance, { + tx: 1, + coin: 1, + unconfirmed: -HARD_FEE + }); + + balances.confirmedBalance = applyDelta(balances.sentBalance, { + confirmed: -HARD_FEE + }); + + balances.unconfirmedBalance = applyDelta(balances.confirmedBalance, { + confirmed: HARD_FEE + }); + + // TODO: Should 0 value outs be counted towards coin and stored in coin set? + balances.eraseBalance = applyDelta(balances.unconfirmedBalance, { + tx: -1, + coin: -1, + unconfirmed: HARD_FEE + }); + + balances.blockConfirmedBalance = balances.confirmedBalance; + balances.blockUnconfirmedBalance = balances.unconfirmedBalance; + balances.blockFinalConfirmedBalance = balances.blockConfirmedBalance; + + await testOpen( + checkBalances(balances), + defDiscover, + DISCOVER_TYPES.NONE + ); + }); + }); + + /* + * Lock balances + */ + + describe('NONE -> BID* (normal receive)', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + let name = null; + const setupBidName = async () => { + name = grindName(GRIND_NAME_LEN, chain.tip.height, network); + + await primary.sendOpen(name, false); + await mineBlocks(openingPeriod); + }; + + const sendNormalBid = async (wallet, clone, ahead) => { + const account = await wallet.getAccount(DEFAULT_ACCOUNT); + const {nextAddr} = getAheadAddr(account, ahead); + const txOpts = { hardFee: HARD_FEE }; + + const bidMTX = await wallet.createBatch([ + ['BID', name, BID_AMOUNT_1, BLIND_AMOUNT_1], + ['BID', name, BID_AMOUNT_2, BLIND_AMOUNT_2] + ], txOpts); + + assert.strictEqual(bidMTX.outputs[0].covenant.type, types.BID); + assert.strictEqual(bidMTX.outputs[1].covenant.type, types.BID); + bidMTX.outputs[1].address = nextAddr; + + await resign(wallet, bidMTX); + node.mempool.addTX(bidMTX.toTX()); + await forWTX(wallet.id, bidMTX.hash()); + }; + + const AHEAD = 10; + const testBidReceive = balanceTest(setupBidName, sendNormalBid, AHEAD); + + // Balances if second BID was undiscovered. + const UNDISCOVERED = {}; + UNDISCOVERED.initialBalance = INIT_BALANCE; + UNDISCOVERED.sentBalance = applyDelta(UNDISCOVERED.initialBalance, { + tx: 1, + // We have additional coin because: output -> BID + Change + // Additional BID is undiscovered. + coin: 1, + // Bid we are not aware of is seen as spent. + unconfirmed: -HARD_FEE - BLIND_AMOUNT_2, + ulocked: BLIND_AMOUNT_1 + }); + + UNDISCOVERED.confirmedBalance = applyDelta(UNDISCOVERED.sentBalance, { + confirmed: -HARD_FEE - BLIND_AMOUNT_2, + clocked: BLIND_AMOUNT_1 + }); + + UNDISCOVERED.unconfirmedBalance = UNDISCOVERED.sentBalance; + UNDISCOVERED.eraseBalance = UNDISCOVERED.initialBalance; + UNDISCOVERED.blockConfirmedBalance = UNDISCOVERED.confirmedBalance; + UNDISCOVERED.blockUnconfirmedBalance = UNDISCOVERED.unconfirmedBalance; + + // Balances if second BID was discovered right away. + const DISCOVERED = {}; + DISCOVERED.initialBalance = UNDISCOVERED.initialBalance; + DISCOVERED.sentBalance = applyDelta(DISCOVERED.initialBalance, { + tx: 1, + coin: 2, + // Bid we are not aware of is seen as spent. + unconfirmed: -HARD_FEE, + ulocked: BLIND_AMOUNT_1 + BLIND_AMOUNT_2 + }); + + DISCOVERED.confirmedBalance = applyDelta(DISCOVERED.sentBalance, { + confirmed: -HARD_FEE, + clocked: BLIND_AMOUNT_1 + BLIND_AMOUNT_2 + }); + + DISCOVERED.unconfirmedBalance = DISCOVERED.sentBalance; + DISCOVERED.eraseBalance = DISCOVERED.initialBalance; + DISCOVERED.blockConfirmedBalance = DISCOVERED.confirmedBalance; + DISCOVERED.blockUnconfirmedBalance = DISCOVERED.unconfirmedBalance; + + genTests({ + name: 'should receive bid', + undiscovered: UNDISCOVERED, + discovered: DISCOVERED, + tester: testBidReceive, + discoverer: defDiscover + }); + }); + + describe('NONE -> BID* (foreign bid)', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + let name = null; + const setupBidName = async () => { + name = grindName(GRIND_NAME_LEN, chain.tip.height, network); + + await primary.sendOpen(name, false); + await mineBlocks(openingPeriod); + }; + + const sendForeignBid = async (wallet, clone, ahead) => { + const account = await wallet.getAccount(DEFAULT_ACCOUNT); + const recvAddr = await wallet.receiveAddress(); + const {nextAddr} = getAheadAddr(account, ahead); + const txOpts = { hardFee: HARD_FEE }; + + const bidMTX = await primary.createBatch([ + ['BID', name, BID_AMOUNT_1, BLIND_AMOUNT_1], + ['BID', name, BID_AMOUNT_2, BLIND_AMOUNT_2] + ], txOpts); + + assert.strictEqual(bidMTX.outputs[0].covenant.type, types.BID); + assert.strictEqual(bidMTX.outputs[1].covenant.type, types.BID); + + bidMTX.outputs[0].address = recvAddr; + bidMTX.outputs[1].address = nextAddr; + + await resign(primary, bidMTX); + node.mempool.addTX(bidMTX.toTX()); + await forWTX(wallet.id, bidMTX.hash()); + }; + + const AHEAD = 10; + const testForeign = balanceTest(setupBidName, sendForeignBid, AHEAD); + + const UNDISCOVERED = {}; + UNDISCOVERED.initialBalance = INIT_BALANCE; + UNDISCOVERED.sentBalance = applyDelta(UNDISCOVERED.initialBalance, { + tx: 1, + // only BID + coin: 1, + // We did not own this money before + unconfirmed: BLIND_AMOUNT_1, + ulocked: BLIND_AMOUNT_1 + }); + + UNDISCOVERED.confirmedBalance = applyDelta(UNDISCOVERED.sentBalance, { + confirmed: BLIND_AMOUNT_1, + clocked: BLIND_AMOUNT_1 + }); + + UNDISCOVERED.unconfirmedBalance = UNDISCOVERED.sentBalance; + UNDISCOVERED.eraseBalance = UNDISCOVERED.initialBalance; + UNDISCOVERED.blockConfirmedBalance = UNDISCOVERED.confirmedBalance; + UNDISCOVERED.blockUnconfirmedBalance = UNDISCOVERED.sentBalance; + + const DISCOVERED = {}; + DISCOVERED.initialBalance = UNDISCOVERED.initialBalance; + DISCOVERED.sentBalance = applyDelta(DISCOVERED.initialBalance, { + tx: 1, + coin: 2, + unconfirmed: BLIND_AMOUNT_1 + BLIND_AMOUNT_2, + ulocked: BLIND_AMOUNT_1 + BLIND_AMOUNT_2 + }); + + DISCOVERED.confirmedBalance = applyDelta(DISCOVERED.sentBalance, { + confirmed: BLIND_AMOUNT_1 + BLIND_AMOUNT_2, + clocked: BLIND_AMOUNT_1 + BLIND_AMOUNT_2 + }); + + DISCOVERED.unconfirmedBalance = DISCOVERED.sentBalance; + DISCOVERED.eraseBalance = DISCOVERED.initialBalance; + DISCOVERED.blockConfirmedBalance = DISCOVERED.confirmedBalance; + DISCOVERED.blockUnconfirmedBalance = DISCOVERED.sentBalance; + + genTests({ + name: 'should receive foreign bid', + undiscovered: UNDISCOVERED, + discovered: DISCOVERED, + tester: testForeign, + discoverer: defDiscover + }); + }); + + describe('NONE -> BID* (cross acct)', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + let name; + const setupAcctAndBidName = async (wallet) => { + await wallet.createAccount({ + name: ALT_ACCOUNT + }); + + name = grindName(GRIND_NAME_LEN, chain.tip.height, network); + + await primary.sendOpen(name, false); + await mineBlocks(openingPeriod); + }; + + const sendCrossAcct = async (wallet, clone, ahead) => { + const txOpts = { hardFee: HARD_FEE }; + const altAccount = await wallet.getAccount(ALT_ACCOUNT); + + // not actually next, we test normal recive. + const addr1 = getAheadAddr(altAccount, -altAccount.lookahead); + const addr2 = getAheadAddr(altAccount, ahead); + + const bidMTX = await wallet.createBatch([ + ['BID', name, BID_AMOUNT_1, BLIND_AMOUNT_1], + ['BID', name, BID_AMOUNT_2, BLIND_AMOUNT_2] + ], txOpts); + + assert.strictEqual(bidMTX.outputs[0].covenant.type, types.BID); + assert.strictEqual(bidMTX.outputs[1].covenant.type, types.BID); + + bidMTX.outputs[0].address = addr1.nextAddr; + // future tx. + bidMTX.outputs[1].address = addr2.nextAddr; + + await resign(wallet, bidMTX); + node.mempool.addTX(bidMTX.toTX()); + await forWTX(wallet.id, bidMTX.hash()); + }; + + const AHEAD = 10; + const testCrossAcctBalance = balanceTest(setupAcctAndBidName, sendCrossAcct, AHEAD); + + const UNDISCOVERED_WALLET = {}; + const UNDISCOVERED_DEFAULT = {}; + const UNDISCOVERED_ALT = {}; + + UNDISCOVERED_WALLET.initialBalance = INIT_BALANCE; + UNDISCOVERED_DEFAULT.initialBalance = INIT_BALANCE; + UNDISCOVERED_ALT.initialBalance = NULL_BALANCE; + + // sent from default to alt, default account does not lock + UNDISCOVERED_DEFAULT.sentBalance = applyDelta(UNDISCOVERED_DEFAULT.initialBalance, { + tx: 1, + // output -> change output + 2 BIDs to alt + coin: 0, + unconfirmed: -HARD_FEE - BLIND_AMOUNT_1 - BLIND_AMOUNT_2 + }); + + // alt account balance locks unconfirmed and receives coin. + UNDISCOVERED_ALT.sentBalance = applyDelta(UNDISCOVERED_ALT.initialBalance, { + tx: 1, + // received BID + missed BID. + coin: 1, + unconfirmed: BLIND_AMOUNT_1, + ulocked: BLIND_AMOUNT_1 + }); + + // Wallet only spends FEE + UNDISCOVERED_WALLET.sentBalance = applyDelta(UNDISCOVERED_WALLET.initialBalance, { + tx: 1, + // Total coins is: output -> BID output + CHANGE + Undiscovered BID + coin: 1, + // for now another bid just out transaction. + unconfirmed: -HARD_FEE - BLIND_AMOUNT_2, + ulocked: BLIND_AMOUNT_1 + }); + + // NOW CONFIRM + UNDISCOVERED_DEFAULT.confirmedBalance = applyDelta(UNDISCOVERED_DEFAULT.sentBalance, { + confirmed: -HARD_FEE - BLIND_AMOUNT_1 - BLIND_AMOUNT_2 + }); + + UNDISCOVERED_ALT.confirmedBalance = applyDelta(UNDISCOVERED_ALT.sentBalance, { + confirmed: BLIND_AMOUNT_1, + clocked: BLIND_AMOUNT_1 + }); + + UNDISCOVERED_WALLET.confirmedBalance = applyDelta(UNDISCOVERED_WALLET.sentBalance, { + confirmed: -HARD_FEE - BLIND_AMOUNT_2, + clocked: BLIND_AMOUNT_1 + }); + + // NOW Unconfirm again + UNDISCOVERED_DEFAULT.unconfirmedBalance = UNDISCOVERED_DEFAULT.sentBalance; + UNDISCOVERED_ALT.unconfirmedBalance = UNDISCOVERED_ALT.sentBalance; + UNDISCOVERED_WALLET.unconfirmedBalance = UNDISCOVERED_WALLET.sentBalance; + + // NOW Erase + UNDISCOVERED_WALLET.eraseBalance = UNDISCOVERED_WALLET.initialBalance; + UNDISCOVERED_DEFAULT.eraseBalance = UNDISCOVERED_DEFAULT.initialBalance; + UNDISCOVERED_ALT.eraseBalance = UNDISCOVERED_ALT.initialBalance; + + UNDISCOVERED_WALLET.blockConfirmedBalance = UNDISCOVERED_WALLET.confirmedBalance; + UNDISCOVERED_DEFAULT.blockConfirmedBalance = UNDISCOVERED_DEFAULT.confirmedBalance; + UNDISCOVERED_ALT.blockConfirmedBalance = UNDISCOVERED_ALT.confirmedBalance; + + UNDISCOVERED_WALLET.blockUnconfirmedBalance = UNDISCOVERED_WALLET.unconfirmedBalance; + UNDISCOVERED_DEFAULT.blockUnconfirmedBalance = UNDISCOVERED_DEFAULT.unconfirmedBalance; + UNDISCOVERED_ALT.blockUnconfirmedBalance = UNDISCOVERED_ALT.unconfirmedBalance; + + // Now DISCOVERED PART + const DISCOVERED_WALLET = {}; + const DISCOVERED_DEFAULT = {}; + const DISCOVERED_ALT = {}; + + DISCOVERED_WALLET.initialBalance = UNDISCOVERED_WALLET.initialBalance; + DISCOVERED_DEFAULT.initialBalance = UNDISCOVERED_DEFAULT.initialBalance; + DISCOVERED_ALT.initialBalance = UNDISCOVERED_ALT.initialBalance; + + // sent from default to alt, default account does not lock + DISCOVERED_DEFAULT.sentBalance = applyDelta(DISCOVERED_DEFAULT.initialBalance, { + tx: 1, + coin: 0, + unconfirmed: -HARD_FEE - BLIND_AMOUNT_1 - BLIND_AMOUNT_2 + }); + + // alt account balance locks unconfirmed and receives coin. + DISCOVERED_ALT.sentBalance = applyDelta(DISCOVERED_ALT.initialBalance, { + tx: 1, + coin: 2, + unconfirmed: BLIND_AMOUNT_1 + BLIND_AMOUNT_2, + ulocked: BLIND_AMOUNT_1 + BLIND_AMOUNT_2 + }); + + // Wallet only spends FEE + DISCOVERED_WALLET.sentBalance = applyDelta(DISCOVERED_WALLET.initialBalance, { + tx: 1, + // Total coins is: output -> BID output + BID output + CHANGE + coin: 2, + unconfirmed: -HARD_FEE, + ulocked: BLIND_AMOUNT_1 + BLIND_AMOUNT_2 + }); + + // NOW CONFIRM + DISCOVERED_DEFAULT.confirmedBalance = applyDelta(DISCOVERED_DEFAULT.sentBalance, { + confirmed: -HARD_FEE - BLIND_AMOUNT_1 - BLIND_AMOUNT_2 + }); + + DISCOVERED_ALT.confirmedBalance = applyDelta(DISCOVERED_ALT.sentBalance, { + confirmed: BLIND_AMOUNT_1 + BLIND_AMOUNT_2, + clocked: BLIND_AMOUNT_1 + BLIND_AMOUNT_2 + }); + + DISCOVERED_WALLET.confirmedBalance = applyDelta(DISCOVERED_WALLET.sentBalance, { + confirmed: -HARD_FEE, + clocked: BLIND_AMOUNT_1 + BLIND_AMOUNT_2 + }); + + // NOW Unconfirm again + DISCOVERED_DEFAULT.unconfirmedBalance = DISCOVERED_DEFAULT.sentBalance; + DISCOVERED_ALT.unconfirmedBalance = DISCOVERED_ALT.sentBalance; + DISCOVERED_WALLET.unconfirmedBalance = DISCOVERED_WALLET.sentBalance; + + // NOW Erase + DISCOVERED_WALLET.eraseBalance = DISCOVERED_WALLET.initialBalance; + DISCOVERED_DEFAULT.eraseBalance = DISCOVERED_DEFAULT.initialBalance; + DISCOVERED_ALT.eraseBalance = DISCOVERED_ALT.initialBalance; + + DISCOVERED_WALLET.blockConfirmedBalance = DISCOVERED_WALLET.confirmedBalance; + DISCOVERED_DEFAULT.blockConfirmedBalance = DISCOVERED_DEFAULT.confirmedBalance; + DISCOVERED_ALT.blockConfirmedBalance = DISCOVERED_ALT.confirmedBalance; + + DISCOVERED_WALLET.blockUnconfirmedBalance = DISCOVERED_WALLET.unconfirmedBalance; + DISCOVERED_DEFAULT.blockUnconfirmedBalance = DISCOVERED_DEFAULT.unconfirmedBalance; + DISCOVERED_ALT.blockUnconfirmedBalance = DISCOVERED_ALT.unconfirmedBalance; + + genTests({ + name: 'should send/receive bid cross acct', + undiscovered: [UNDISCOVERED_WALLET, UNDISCOVERED_DEFAULT, UNDISCOVERED_ALT], + discovered: [DISCOVERED_WALLET, DISCOVERED_DEFAULT, DISCOVERED_ALT], + tester: testCrossAcctBalance, + discoverer: altDiscover + }); + }); + + describe('BID* -> REVEAL*', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + let name = null; + + const setupBidName = async (wallet, clone, ahead) => { + name = grindName(GRIND_NAME_LEN, chain.tip.height, network); + const txOpts = { hardFee: HARD_FEE }; + + const account = await wallet.getAccount(DEFAULT_ACCOUNT); + const next = getAheadAddr(account, ahead, wallet.master); + const {nextAddr} = next; + + await primary.sendOpen(name, false); + await mineBlocks(openingPeriod); + + const bidMTX = await clone.createBatch([ + ['BID', name, BID_AMOUNT_1, BLIND_AMOUNT_1], + ['BID', name, BID_AMOUNT_2, BLIND_AMOUNT_2] + ], txOpts); + + assert.strictEqual(bidMTX.outputs[0].covenant.type, types.BID); + assert.strictEqual(bidMTX.outputs[1].covenant.type, types.BID); + bidMTX.outputs[1].address = nextAddr; + + await resign(clone, bidMTX); + + // Make sure we discover everything + await defDiscover(clone, ahead * 2); + + node.mempool.addTX(bidMTX.toTX()); + await forWTX(wallet.id, bidMTX.hash()); + await mineBlocks(biddingPeriod); + }; + + const sendReveal = async (wallet, clone, ahead) => { + await clone.sendReveal(name, { + hardFee: HARD_FEE + }); + }; + + const AHEAD = 10; + const testReveal = balanceTest(setupBidName, sendReveal, AHEAD); + + const UNDISCOVERED = {}; + UNDISCOVERED.initialBalance = applyDelta(INIT_BALANCE, { + tx: 1, + // out = BID + Unknown BID + CHANGE + coin: 1, + + // one bid is unknown + confirmed: -HARD_FEE - BLIND_AMOUNT_2, + unconfirmed: -HARD_FEE - BLIND_AMOUNT_2, + + // one bid is unknown + clocked: BLIND_AMOUNT_1, + ulocked: BLIND_AMOUNT_1 + }); + + // Now we receive REVEAL - which frees BLIND and only locks bid amount. + UNDISCOVERED.sentBalance = applyDelta(UNDISCOVERED.initialBalance, { + tx: 1, + // extra coin from Change + coin: 1, + // We recover BLIND_ONLY from the unknown BID via change. + unconfirmed: BLIND_ONLY_2 - HARD_FEE, + ulocked: -BLIND_ONLY_1 + }); + + UNDISCOVERED.confirmedBalance = applyDelta(UNDISCOVERED.sentBalance, { + confirmed: BLIND_ONLY_2 - HARD_FEE, + clocked: -BLIND_ONLY_1 + }); + + UNDISCOVERED.unconfirmedBalance = UNDISCOVERED.sentBalance; + UNDISCOVERED.eraseBalance = UNDISCOVERED.initialBalance; + UNDISCOVERED.blockConfirmedBalance = UNDISCOVERED.confirmedBalance; + UNDISCOVERED.blockUnconfirmedBalance = UNDISCOVERED.unconfirmedBalance; + + const DISCOVERED = {}; + DISCOVERED.initialBalance = UNDISCOVERED.initialBalance; + + // Now we receive REVEAL - which frees BLIND and only locks bid amount. + DISCOVERED.sentBalance = applyDelta(DISCOVERED.initialBalance, { + tx: 1, + coin: 2, + unconfirmed: BLIND_AMOUNT_2 - HARD_FEE, + ulocked: BID_AMOUNT_2 - BLIND_ONLY_1 + }); + + DISCOVERED.confirmedBalance = applyDelta(DISCOVERED.sentBalance, { + confirmed: BLIND_AMOUNT_2 - HARD_FEE, + clocked: BID_AMOUNT_2 - BLIND_ONLY_1 + }); + + DISCOVERED.unconfirmedBalance = DISCOVERED.sentBalance; + DISCOVERED.eraseBalance = DISCOVERED.initialBalance; + DISCOVERED.blockConfirmedBalance = DISCOVERED.confirmedBalance; + DISCOVERED.blockUnconfirmedBalance = DISCOVERED.unconfirmedBalance; + + genTests({ + name: 'should send/receive reveal', + undiscovered: UNDISCOVERED, + discovered: DISCOVERED, + tester: testReveal, + discoverer: defDiscover + }); + }); + + describe('BID* -> REVEAL* (cross acct)', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + let name; + + // This will create BID tx in the first account. + // two bids belong to the default account. + const setupRevealName = async (wallet) => { + name = grindName(GRIND_NAME_LEN, chain.tip.height, network); + + await wallet.createAccount({ + name: ALT_ACCOUNT + }); + const txOpts = { hardFee: HARD_FEE }; + + await primary.sendOpen(name, false); + await mineBlocks(openingPeriod); + + await wallet.sendBatch([ + ['BID', name, BID_AMOUNT_1, BLIND_AMOUNT_1], + ['BID', name, BID_AMOUNT_2, BLIND_AMOUNT_2] + ], txOpts); + await mineBlocks(biddingPeriod); + }; + + // Now we sent two REVEALs to second account (one seen, one missed) + const sendReveal = async (wallet, clone, ahead) => { + const altAccount = await wallet.getAccount(ALT_ACCOUNT); + const recv = getAheadAddr(altAccount, -altAccount.lookahead); + const next = getAheadAddr(altAccount, ahead); + + const revealMTX = await wallet.createReveal(name, { + hardFee: HARD_FEE + }); + assert.strictEqual(revealMTX.outputs[0].covenant.type, types.REVEAL); + assert.strictEqual(revealMTX.outputs[1].covenant.type, types.REVEAL); + revealMTX.outputs[0].address = recv.nextAddr; + revealMTX.outputs[1].address = next.nextAddr; + + await resign(wallet, revealMTX); + node.mempool.addTX(revealMTX.toTX()); + await forWTX(wallet.id, revealMTX.hash()); + }; + + const AHEAD = 10; + const testCrossActReveal = balanceTest(setupRevealName, sendReveal, AHEAD); + + /* + * Balances if we never discovered missing. + */ + + const UNDISCOVERED_WALLET = {}; + const UNDISCOVERED_DEFAULT = {}; + const UNDISCOVERED_ALT = {}; + + // we start with BID transaction + UNDISCOVERED_WALLET.initialBalance = applyDelta(INIT_BALANCE, { + tx: 1, + + // we have two bids at the start. + coin: 2, + + confirmed: -HARD_FEE, + unconfirmed: -HARD_FEE, + + clocked: BLIND_AMOUNT_1 + BLIND_AMOUNT_2, + ulocked: BLIND_AMOUNT_1 + BLIND_AMOUNT_2 + }); + + // same as wallet at this stage. + UNDISCOVERED_DEFAULT.initialBalance = UNDISCOVERED_WALLET.initialBalance; + // empty at the start. + UNDISCOVERED_ALT.initialBalance = NULL_BALANCE; + + // After REVEAL Transaction + UNDISCOVERED_WALLET.sentBalance = applyDelta(UNDISCOVERED_WALLET.initialBalance, { + tx: 1, + // extra coin from change. + // but one reveal becomes missed. + coin: 0, + + // We only lose reveal amount, diff is going into our change + unconfirmed: -BID_AMOUNT_2 - HARD_FEE, + // We also unlock missed bid->reveal, + // but totally unlock missed one. + ulocked: -BLIND_ONLY_1 - BLIND_AMOUNT_2 + }); + + // does not change + UNDISCOVERED_DEFAULT.sentBalance = applyDelta(UNDISCOVERED_DEFAULT.initialBalance, { + tx: 1, + // 2 BIDS -> 1 Change + out 2 reveals + coin: -1, + + unconfirmed: -BID_AMOUNT_1 - BID_AMOUNT_2 - HARD_FEE, + ulocked: -BLIND_AMOUNT_1 - BLIND_AMOUNT_2 + }); + + UNDISCOVERED_ALT.sentBalance = applyDelta(UNDISCOVERED_ALT.initialBalance, { + tx: 1, + // we received 1 reveal (another is unknown) + coin: 1, + + unconfirmed: BID_AMOUNT_1, + ulocked: BID_AMOUNT_1 + }); + + // Now we confirm everything seen above. + UNDISCOVERED_WALLET.confirmedBalance = applyDelta(UNDISCOVERED_WALLET.sentBalance, { + confirmed: -BID_AMOUNT_2 - HARD_FEE, + clocked: -BLIND_ONLY_1 - BLIND_AMOUNT_2 + }); + + UNDISCOVERED_DEFAULT.confirmedBalance = applyDelta(UNDISCOVERED_DEFAULT.sentBalance, { + confirmed: -BID_AMOUNT_1 - BID_AMOUNT_2 - HARD_FEE, + clocked: -BLIND_AMOUNT_1 - BLIND_AMOUNT_2 + }); + + UNDISCOVERED_ALT.confirmedBalance = applyDelta(UNDISCOVERED_ALT.sentBalance, { + confirmed: BID_AMOUNT_1, + clocked: BID_AMOUNT_1 + }); + + UNDISCOVERED_WALLET.unconfirmedBalance = UNDISCOVERED_WALLET.sentBalance; + UNDISCOVERED_DEFAULT.unconfirmedBalance = UNDISCOVERED_DEFAULT.sentBalance; + UNDISCOVERED_ALT.unconfirmedBalance = UNDISCOVERED_ALT.sentBalance; + + // Erase + UNDISCOVERED_WALLET.eraseBalance = UNDISCOVERED_WALLET.initialBalance; + UNDISCOVERED_DEFAULT.eraseBalance = UNDISCOVERED_DEFAULT.initialBalance; + UNDISCOVERED_ALT.eraseBalance = UNDISCOVERED_ALT.initialBalance; + + // Confirm in block + UNDISCOVERED_WALLET.blockConfirmedBalance = UNDISCOVERED_WALLET.confirmedBalance; + UNDISCOVERED_DEFAULT.blockConfirmedBalance = UNDISCOVERED_DEFAULT.confirmedBalance; + UNDISCOVERED_ALT.blockConfirmedBalance = UNDISCOVERED_ALT.confirmedBalance; + + // Unconfirm in block + UNDISCOVERED_WALLET.blockUnconfirmedBalance = UNDISCOVERED_WALLET.unconfirmedBalance; + UNDISCOVERED_DEFAULT.blockUnconfirmedBalance = UNDISCOVERED_DEFAULT.unconfirmedBalance; + UNDISCOVERED_ALT.blockUnconfirmedBalance = UNDISCOVERED_ALT.unconfirmedBalance; + + /* + * Balances if we had discovered it right away. + */ + + const DISCOVERED_WALLET = {}; + const DISCOVERED_DEFAULT = {}; + const DISCOVERED_ALT = {}; + + DISCOVERED_WALLET.initialBalance = UNDISCOVERED_WALLET.initialBalance;; + // same as wallet at this stage. + DISCOVERED_DEFAULT.initialBalance = UNDISCOVERED_DEFAULT.initialBalance; + // empty at the start. + DISCOVERED_ALT.initialBalance = UNDISCOVERED_ALT.initialBalance; + + // After REVEAL Transaction + DISCOVERED_WALLET.sentBalance = applyDelta(DISCOVERED_WALLET.initialBalance, { + tx: 1, + // extra change introduce by reveal tx. + coin: 1, + + unconfirmed: -HARD_FEE, + // unlock blinds, only BID are left locked. + ulocked: -BLIND_ONLY_1 - BLIND_ONLY_2 + }); + + // does not change + DISCOVERED_DEFAULT.sentBalance = applyDelta(DISCOVERED_DEFAULT.initialBalance, { + tx: 1, + // 2 BIDS -> 1 Change + out 2 reveals + coin: -1, + + unconfirmed: -BID_AMOUNT_1 - BID_AMOUNT_2 - HARD_FEE, + ulocked: -BLIND_AMOUNT_1 - BLIND_AMOUNT_2 + }); + + DISCOVERED_ALT.sentBalance = applyDelta(DISCOVERED_ALT.initialBalance, { + tx: 1, + // we received 2 reveal + coin: 2, + + unconfirmed: BID_AMOUNT_1 + BID_AMOUNT_2, + ulocked: BID_AMOUNT_1 + BID_AMOUNT_2 + }); + + // Now we confirm everything seen above. + DISCOVERED_WALLET.confirmedBalance = applyDelta(DISCOVERED_WALLET.sentBalance, { + confirmed: -HARD_FEE, + clocked: -BLIND_ONLY_1 - BLIND_ONLY_2 + }); + + DISCOVERED_DEFAULT.confirmedBalance = applyDelta(DISCOVERED_DEFAULT.sentBalance, { + confirmed: -BID_AMOUNT_1 - BID_AMOUNT_2 - HARD_FEE, + clocked: -BLIND_AMOUNT_1 - BLIND_AMOUNT_2 + }); + + DISCOVERED_ALT.confirmedBalance = applyDelta(DISCOVERED_ALT.sentBalance, { + confirmed: BID_AMOUNT_1 + BID_AMOUNT_2, + clocked: BID_AMOUNT_1 + BID_AMOUNT_2 + }); + + DISCOVERED_WALLET.unconfirmedBalance = DISCOVERED_WALLET.sentBalance; + DISCOVERED_DEFAULT.unconfirmedBalance = DISCOVERED_DEFAULT.sentBalance; + DISCOVERED_ALT.unconfirmedBalance = DISCOVERED_ALT.sentBalance; + + // Erase + DISCOVERED_WALLET.eraseBalance = DISCOVERED_WALLET.initialBalance; + DISCOVERED_DEFAULT.eraseBalance = DISCOVERED_DEFAULT.initialBalance; + DISCOVERED_ALT.eraseBalance = DISCOVERED_ALT.initialBalance; + + // Confirm in block + DISCOVERED_WALLET.blockConfirmedBalance = DISCOVERED_WALLET.confirmedBalance; + DISCOVERED_DEFAULT.blockConfirmedBalance = DISCOVERED_DEFAULT.confirmedBalance; + DISCOVERED_ALT.blockConfirmedBalance = DISCOVERED_ALT.confirmedBalance; + + // Unconfirm in block + DISCOVERED_WALLET.blockUnconfirmedBalance = DISCOVERED_WALLET.unconfirmedBalance; + DISCOVERED_DEFAULT.blockUnconfirmedBalance = DISCOVERED_DEFAULT.unconfirmedBalance; + DISCOVERED_ALT.blockUnconfirmedBalance = DISCOVERED_ALT.unconfirmedBalance; + + genTests({ + name: 'should send/receive reveal', + undiscovered: [UNDISCOVERED_WALLET, UNDISCOVERED_DEFAULT, UNDISCOVERED_ALT], + discovered: [DISCOVERED_WALLET, DISCOVERED_DEFAULT, DISCOVERED_ALT], + tester: testCrossActReveal, + discoverer: altDiscover + }); + }); + + describe('BID -> REVEAL* (foreign reveal)', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + let name; + const setupRevealName = async () => { + name = grindName(GRIND_NAME_LEN, chain.tip.height, network); + + await primary.sendOpen(name, false); + await mineBlocks(openingPeriod); + await primary.sendBatch([ + ['BID', name, BID_AMOUNT_1, BLIND_AMOUNT_1], + ['BID', name, BID_AMOUNT_2, BLIND_AMOUNT_2] + ]); + await mineBlocks(biddingPeriod); + }; + + const sendReveal = async (wallet, clone, ahead) => { + const account = await wallet.getAccount(DEFAULT_ACCOUNT); + const {nextAddr} = getAheadAddr(account, ahead); + const recv = await wallet.receiveAddress(); + + const mtx = await primary.createReveal(name); + assert.strictEqual(mtx.outputs[0].covenant.type, types.REVEAL); + assert.strictEqual(mtx.outputs[1].covenant.type, types.REVEAL); + + mtx.outputs[0].address = recv; + mtx.outputs[1].address = nextAddr; + await resign(primary, mtx); + + node.mempool.addTX(mtx.toTX()); + await forWTX(wallet.id, mtx.hash()); + }; + + const AHEAD = 10; + const testForeignReveal = balanceTest(setupRevealName, sendReveal, AHEAD); + + // balances if missing reveal was not discovered. + const UNDISCOVERED = {}; + UNDISCOVERED.initialBalance = INIT_BALANCE; + UNDISCOVERED.sentBalance = applyDelta(UNDISCOVERED.initialBalance, { + tx: 1, + coin: 1, + + unconfirmed: BID_AMOUNT_1, + ulocked: BID_AMOUNT_1 + }); + + UNDISCOVERED.confirmedBalance = applyDelta(UNDISCOVERED.sentBalance, { + confirmed: BID_AMOUNT_1, + clocked: BID_AMOUNT_1 + }); + + UNDISCOVERED.unconfirmedBalance = UNDISCOVERED.sentBalance; + UNDISCOVERED.eraseBalance = UNDISCOVERED.initialBalance; + UNDISCOVERED.blockConfirmedBalance = UNDISCOVERED.confirmedBalance; + UNDISCOVERED.blockUnconfirmedBalance = UNDISCOVERED.unconfirmedBalance; + + // Balances if everyting was discovered from the begining. + const DISCOVERED = {}; + DISCOVERED.initialBalance = UNDISCOVERED.initialBalance; + DISCOVERED.sentBalance = applyDelta(DISCOVERED.initialBalance, { + tx: 1, + coin: 2, + + unconfirmed: BID_AMOUNT_1 + BID_AMOUNT_2, + ulocked: BID_AMOUNT_1 + BID_AMOUNT_2 + }); + + DISCOVERED.confirmedBalance = applyDelta(DISCOVERED.sentBalance, { + confirmed: BID_AMOUNT_1 + BID_AMOUNT_2, + clocked: BID_AMOUNT_1 + BID_AMOUNT_2 + }); + + DISCOVERED.unconfirmedBalance = DISCOVERED.sentBalance; + DISCOVERED.eraseBalance = DISCOVERED.initialBalance; + DISCOVERED.blockConfirmedBalance = DISCOVERED.confirmedBalance; + DISCOVERED.blockUnconfirmedBalance = DISCOVERED.sentBalance; + + genTests({ + name: 'should send/receive reveal', + undiscovered: UNDISCOVERED, + discovered: DISCOVERED, + tester: testForeignReveal, + discoverer: defDiscover + }); + }); + + describe('REVEAL* -> REDEEM*', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + /* + * TODO: Move this tests to the auction tests. + * - 1 normal -> redeem (loser) + * - 1 missed -> redeem (loser) - bid chain is missing until reve + */ + + let name1, name2; + + const setupRevealNames = async (wallet, clone, ahead) => { + name1 = grindName(GRIND_NAME_LEN, chain.tip.height, network); + name2 = grindName(GRIND_NAME_LEN, chain.tip.height, network); + + const cloneAccount = await clone.getAccount(DEFAULT_ACCOUNT); + const addr1 = getAheadAddr(cloneAccount, ahead); + + await primary.sendBatch([ + ['OPEN', name1], + ['OPEN', name2] + ]); + await mineBlocks(openingPeriod); + + // primary will win + await primary.sendBid(name1, INIT_FUND, INIT_FUND); + await primary.sendBid(name2, INIT_FUND, INIT_FUND); + + const txOpts = { hardFee: HARD_FEE }; + + // all three bids are there. + const bidMTX = await clone.createBatch([ + ['BID', name1, BID_AMOUNT_1, BLIND_AMOUNT_1], + ['BID', name2, BID_AMOUNT_2, BLIND_AMOUNT_2] + ], txOpts); + + assert.strictEqual(bidMTX.outputs[0].covenant.type, types.BID); + assert.strictEqual(bidMTX.outputs[1].covenant.type, types.BID); + + bidMTX.outputs[1].address = addr1.nextAddr; + await resign(clone, bidMTX); + + // make sure clone knows ahead addrs. + await defDiscover(clone, ahead * 2); + + await node.mempool.addTX(bidMTX.toTX()); + await mineBlocks(biddingPeriod); + + await primary.sendReveal(name1); + await primary.sendReveal(name2); + + await clone.sendBatch([ + ['REVEAL', name1], + ['REVEAL', name2] + ], txOpts); + + await mineBlocks(revealPeriod + 1); + }; + + const sendRedeems = async (wallet, clone, ahead) => { + await clone.sendBatch([ + ['REDEEM', name1], + ['REDEEM', name2] + ], { + hardFee: HARD_FEE + }); + }; + + const AHEAD = 10; + const testRevealRedeems = balanceTest(setupRevealNames, sendRedeems, AHEAD); + + const UNDISCOVERED = {}; + UNDISCOVERED.initialBalance = applyDelta(INIT_BALANCE, { + // 1 for bid, 1 for reveal. + tx: 2, + + // (1 coin -> 1 change = 0) + 1 change + 1 reveal + coin: 2, + + // Does not know about 1 reveal, so it is an out. + confirmed: -(HARD_FEE * 2) - BID_AMOUNT_2, + unconfirmed: -(HARD_FEE * 2) - BID_AMOUNT_2, + + // only aware of single reveal. + clocked: BID_AMOUNT_1, + ulocked: BID_AMOUNT_1 + }); + + UNDISCOVERED.sentBalance = applyDelta(UNDISCOVERED.initialBalance, { + // redeem tx + tx: 1, + + coin: 0, + + unconfirmed: -HARD_FEE, + ulocked: -BID_AMOUNT_1 + }); + + UNDISCOVERED.confirmedBalance = applyDelta(UNDISCOVERED.sentBalance, { + confirmed: -HARD_FEE, + clocked: -BID_AMOUNT_1 + }); + + UNDISCOVERED.unconfirmedBalance = UNDISCOVERED.sentBalance; + UNDISCOVERED.eraseBalance = UNDISCOVERED.initialBalance; + UNDISCOVERED.blockConfirmedBalance = UNDISCOVERED.confirmedBalance; + UNDISCOVERED.blockUnconfirmedBalance = UNDISCOVERED.unconfirmedBalance; + + const DISCOVERED = {}; + DISCOVERED.initialBalance = UNDISCOVERED.initialBalance; + + DISCOVERED.sentBalance = applyDelta(DISCOVERED.initialBalance, { + tx: 1, + coin: 1, + unconfirmed: -HARD_FEE + BID_AMOUNT_2, + ulocked: -BID_AMOUNT_1 + }); + + DISCOVERED.confirmedBalance = applyDelta(DISCOVERED.sentBalance, { + confirmed: -HARD_FEE + BID_AMOUNT_2, + clocked: -BID_AMOUNT_1 + }); + + DISCOVERED.unconfirmedBalance = DISCOVERED.sentBalance; + DISCOVERED.eraseBalance = DISCOVERED.initialBalance; + DISCOVERED.blockConfirmedBalance = DISCOVERED.confirmedBalance; + DISCOVERED.blockUnconfirmedBalance = DISCOVERED.unconfirmedBalance; + + genTests({ + name: 'should send/receive reveal->redeem', + undiscovered: UNDISCOVERED, + discovered: DISCOVERED, + tester: testRevealRedeems, + discoverer: defDiscover + }); + }); + + describe('REVEAL* -> REGISTER*', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + /* + * TODO: Move this tests to the auction tests. + * - 1 normal -> register (WINNER) + * - 1 missed -> register (WINNER) - bid chain is missing until reve + */ + + let name1, name2; + + const setupRevealNames = async (wallet, clone, ahead) => { + const names = await setupTwoRegisteredNames(clone, ahead, false); + + name1 = names[0]; + name2 = names[1]; + }; + + const sendRedeems = async (wallet, clone, ahead) => { + await clone.sendBatch([ + ['UPDATE', name1, EMPTY_RS], + ['UPDATE', name2, EMPTY_RS] + ], { + hardFee: HARD_FEE + }); + }; + + const AHEAD = 10; + const testRevealRedeems = balanceTest(setupRevealNames, sendRedeems, AHEAD); + + const UNDISCOVERED = {}; + UNDISCOVERED.initialBalance = applyDelta(INIT_BALANCE, { + // 1 for bid, 1 for reveal. + tx: 2, + + // (1 coin -> 1 change = 0) + 1 change + 1 reveal + coin: 2, + + // Does not know about 1 reveal, so it is an out. + confirmed: -(HARD_FEE * 2) - BID_AMOUNT_2, + unconfirmed: -(HARD_FEE * 2) - BID_AMOUNT_2, + + // only aware of single reveal. + clocked: BID_AMOUNT_1, + ulocked: BID_AMOUNT_1 + }); + + UNDISCOVERED.sentBalance = applyDelta(UNDISCOVERED.initialBalance, { + // register tx + tx: 1, + // additional change for REGISTER tx. + coin: 1, + + // BID_AMOUNT_2 was returned via change + // only finalPrice2 is not accounted for. + unconfirmed: -HARD_FEE + BID_AMOUNT_2 - FINAL_PRICE_2, + ulocked: -BID_AMOUNT_1 + FINAL_PRICE_1 + }); + + UNDISCOVERED.confirmedBalance = applyDelta(UNDISCOVERED.sentBalance, { + confirmed: -HARD_FEE + BID_AMOUNT_2 - FINAL_PRICE_2, + clocked: -BID_AMOUNT_1 + FINAL_PRICE_1 + }); + + UNDISCOVERED.unconfirmedBalance = UNDISCOVERED.sentBalance; + UNDISCOVERED.eraseBalance = UNDISCOVERED.initialBalance; + UNDISCOVERED.blockConfirmedBalance = UNDISCOVERED.confirmedBalance; + UNDISCOVERED.blockUnconfirmedBalance = UNDISCOVERED.unconfirmedBalance; + + const DISCOVERED = {}; + DISCOVERED.initialBalance = UNDISCOVERED.initialBalance; + + DISCOVERED.sentBalance = applyDelta(DISCOVERED.initialBalance, { + tx: 1, + coin: 2, + unconfirmed: -HARD_FEE + BID_AMOUNT_2, + ulocked: -BID_AMOUNT_1 + FINAL_PRICE_1 + FINAL_PRICE_2 + }); + + DISCOVERED.confirmedBalance = applyDelta(DISCOVERED.sentBalance, { + confirmed: -HARD_FEE + BID_AMOUNT_2, + clocked: -BID_AMOUNT_1 + FINAL_PRICE_1 + FINAL_PRICE_2 + }); + + DISCOVERED.unconfirmedBalance = DISCOVERED.sentBalance; + DISCOVERED.eraseBalance = DISCOVERED.initialBalance; + DISCOVERED.blockConfirmedBalance = DISCOVERED.confirmedBalance; + DISCOVERED.blockUnconfirmedBalance = DISCOVERED.unconfirmedBalance; + + genTests({ + name: 'should send/receive reveal->register', + undiscovered: UNDISCOVERED, + discovered: DISCOVERED, + tester: testRevealRedeems, + discoverer: defDiscover + }); + }); + + /* + * All updates types have the same accounting outcomes + */ + + const UPDATE_UNDISCOVERED = {}; + UPDATE_UNDISCOVERED.initialBalance = INIT_REGISTERED_BALANCE; + UPDATE_UNDISCOVERED.sentBalance = applyDelta(UPDATE_UNDISCOVERED.initialBalance, { + tx: 1, + unconfirmed: -HARD_FEE + }); + + UPDATE_UNDISCOVERED.confirmedBalance = applyDelta(UPDATE_UNDISCOVERED.sentBalance, { + confirmed: -HARD_FEE + }); + + UPDATE_UNDISCOVERED.unconfirmedBalance = UPDATE_UNDISCOVERED.sentBalance; + UPDATE_UNDISCOVERED.eraseBalance = UPDATE_UNDISCOVERED.initialBalance; + UPDATE_UNDISCOVERED.blockConfirmedBalance = UPDATE_UNDISCOVERED.confirmedBalance; + UPDATE_UNDISCOVERED.blockUnconfirmedBalance = UPDATE_UNDISCOVERED.unconfirmedBalance; + + const UPDATE_DISCOVERED = {}; + UPDATE_DISCOVERED.initialBalance = UPDATE_UNDISCOVERED.initialBalance; + + UPDATE_DISCOVERED.sentBalance = applyDelta(UPDATE_DISCOVERED.initialBalance, { + tx: 1, + // discovers the unknown update + coin: 1, + + unconfirmed: -HARD_FEE + FINAL_PRICE_2, + ulocked: FINAL_PRICE_2 + }); + + UPDATE_DISCOVERED.confirmedBalance = applyDelta(UPDATE_DISCOVERED.sentBalance, { + confirmed: -HARD_FEE + FINAL_PRICE_2, + clocked: FINAL_PRICE_2 + }); + + UPDATE_DISCOVERED.unconfirmedBalance = UPDATE_DISCOVERED.sentBalance; + UPDATE_DISCOVERED.eraseBalance = UPDATE_DISCOVERED.initialBalance; + UPDATE_DISCOVERED.blockConfirmedBalance = UPDATE_DISCOVERED.confirmedBalance; + UPDATE_DISCOVERED.blockUnconfirmedBalance = UPDATE_DISCOVERED.unconfirmedBalance; + + describe('REGISTER* -> UPDATE*', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + let name1, name2; + + const setupRegisteredNames = async (wallet, clone, ahead) => { + const names = await setupTwoRegisteredNames(clone, ahead); + + name1 = names[0]; + name2 = names[1]; + }; + + const sendUpdates = async (wallet, clone) => { + await clone.sendBatch([ + ['UPDATE', name1, EMPTY_RS], + ['UPDATE', name2, EMPTY_RS] + ], { + hardFee: HARD_FEE + }); + }; + + const AHEAD = 10; + const testSendUpdate = balanceTest(setupRegisteredNames, sendUpdates, AHEAD); + + genTests({ + name: 'should send/receive register->update', + undiscovered: UPDATE_UNDISCOVERED, + discovered: UPDATE_DISCOVERED, + tester: testSendUpdate, + discoverer: defDiscover + }); + }); + + // NOTE: Revokes are permanently burned coins, should we discount them from + // balance and UTXO set? (moved to burned balance) + describe('REGISTER/UPDATE* -> REVOKE*', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + let name1, name2; + + const setupRegisteredNames = async (wallet, clone, ahead) => { + const names = await setupTwoRegisteredNames(clone, ahead); + + name1 = names[0]; + name2 = names[1]; + }; + + const sendRevokes = async (wallet, clone) => { + await clone.sendBatch([ + ['REVOKE', name1], + ['REVOKE', name2] + ], { + hardFee: HARD_FEE + }); + }; + + const AHEAD = 10; + const testSendRevokes = balanceTest(setupRegisteredNames, sendRevokes, AHEAD); + + genTests({ + name: 'should send/receive register->revoke', + undiscovered: UPDATE_UNDISCOVERED, + discovered: UPDATE_DISCOVERED, + tester: testSendRevokes, + discoverer: defDiscover + }); + }); + + describe('REGISTER/UPDATE* -> RENEW*', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + let name1, name2; + + const setupRegisteredNames = async (wallet, clone, ahead) => { + const names = await setupTwoRegisteredNames(clone, ahead); + + name1 = names[0]; + name2 = names[1]; + }; + + const sendRenews = async (wallet, clone) => { + await mineBlocks(treeInterval); + await clone.sendBatch([ + ['RENEW', name1], + ['RENEW', name2] + ], { + hardFee: HARD_FEE + }); + }; + + const AHEAD = 10; + const testSendRenews = balanceTest(setupRegisteredNames, sendRenews, AHEAD); + + genTests({ + name: 'should send/receive register->renew', + undiscovered: UPDATE_UNDISCOVERED, + discovered: UPDATE_DISCOVERED, + tester: testSendRenews, + discoverer: defDiscover + }); + }); + + describe('REGISTER/UPDATE* -> TRANSFER*', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + let name1, name2; + + const setupRegisteredNames = async (wallet, clone, ahead) => { + const names = await setupTwoRegisteredNames(clone, ahead); + + name1 = names[0]; + name2 = names[1]; + }; + + const sendTransfers = async (wallet, clone) => { + await clone.sendBatch([ + ['TRANSFER', name1, await primary.receiveAddress()], + ['TRANSFER', name2, await primary.receiveAddress()] + ], { + hardFee: HARD_FEE + }); + }; + + const AHEAD = 10; + const testSendTransfer = balanceTest(setupRegisteredNames, sendTransfers, AHEAD); + + genTests({ + name: 'should send/receive register->renew', + undiscovered: UPDATE_UNDISCOVERED, + discovered: UPDATE_DISCOVERED, + tester: testSendTransfer, + discoverer: defDiscover + }); + }); + + describe('TRANSFER* -> FINALIZE', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + let name1, name2; + + const setupTransferNames = async (wallet, clone, ahead) => { + const names = await setupTwoRegisteredNames(clone, ahead); + + name1 = names[0]; + name2 = names[1]; + + await clone.sendBatch([ + ['TRANSFER', name1, await primary.receiveAddress()], + ['TRANSFER', name2, await primary.receiveAddress()] + ], { + hardFee: HARD_FEE + }); + + await mineBlocks(transferLockup); + }; + + const sendFinalizes = async (wallet, clone) => { + await clone.sendBatch([ + ['FINALIZE', name1], + ['FINALIZE', name2] + ], { + hardFee: HARD_FEE + }); + }; + + const AHEAD = 10; + const testSendFinalizes = balanceTest(setupTransferNames, sendFinalizes, AHEAD); + + const UNDISCOVERED = {}; + UNDISCOVERED.initialBalance = applyDelta(INIT_REGISTERED_BALANCE, { + // we sent TRANSFER + tx: 1, + confirmed: -HARD_FEE, + unconfirmed: -HARD_FEE + }); + + UNDISCOVERED.sentBalance = applyDelta(UNDISCOVERED.initialBalance, { + tx: 1, + coin: -1, + unconfirmed: -FINAL_PRICE_1 - HARD_FEE, + ulocked: -FINAL_PRICE_1 + }); + + UNDISCOVERED.confirmedBalance = applyDelta(UNDISCOVERED.sentBalance, { + confirmed: -FINAL_PRICE_1 - HARD_FEE, + clocked: -FINAL_PRICE_1 + }); + + UNDISCOVERED.unconfirmedBalance = UNDISCOVERED.sentBalance; + UNDISCOVERED.eraseBalance = UNDISCOVERED.initialBalance; + UNDISCOVERED.blockConfirmedBalance = UNDISCOVERED.confirmedBalance; + UNDISCOVERED.blockUnconfirmedBalance = UNDISCOVERED.unconfirmedBalance; + + const DISCOVERED = {}; + DISCOVERED.initialBalance = UNDISCOVERED.initialBalance; + DISCOVERED.sentBalance = applyDelta(DISCOVERED.initialBalance, { + tx: 1, + coin: -1, + + // Because we only discover when it's outgoing, it wont affect our balance. + unconfirmed: -FINAL_PRICE_1 - HARD_FEE, + ulocked: -FINAL_PRICE_1 + }); + + DISCOVERED.confirmedBalance = applyDelta(DISCOVERED.sentBalance, { + confirmed: -FINAL_PRICE_1 - HARD_FEE, + clocked: -FINAL_PRICE_1 + }); + + DISCOVERED.unconfirmedBalance = DISCOVERED.sentBalance; + DISCOVERED.eraseBalance = DISCOVERED.initialBalance; + DISCOVERED.blockConfirmedBalance = DISCOVERED.confirmedBalance; + DISCOVERED.blockUnconfirmedBalance = DISCOVERED.unconfirmedBalance; + + genTests({ + name: 'should send finalize', + undiscovered: UNDISCOVERED, + discovered: DISCOVERED, + tester: testSendFinalizes, + discoverer: defDiscover + }); + }); + + describe('TRANSFER* -> FINALIZE* (cross acct)', function() { + this.timeout(5000); + before(() => { + genWallets = 6; + return beforeAll(); + }); + + after(afterAll); + + let name1, name2; + + const setupTransferNames = async (wallet, clone, ahead) => { + await wallet.createAccount({ + name: ALT_ACCOUNT + }); + + const altAccount = await wallet.getAccount(ALT_ACCOUNT); + const recv = await wallet.receiveAddress(ALT_ACCOUNT); + const {nextAddr} = getAheadAddr(altAccount, ahead); + + const names = await setupTwoRegisteredNames(clone, ahead); + + name1 = names[0]; + name2 = names[1]; + + await clone.sendBatch([ + ['TRANSFER', name1, recv], + ['TRANSFER', name2, nextAddr] + ], { + hardFee: HARD_FEE + }); + + await mineBlocks(transferLockup); + }; + + const sendFinalizes = async (wallet, clone) => { + await clone.sendBatch([ + ['FINALIZE', name1], + ['FINALIZE', name2] + ], { + hardFee: HARD_FEE + }); + }; + + const AHEAD = 10; + const testSendFinalizes = balanceTest(setupTransferNames, sendFinalizes, AHEAD); + + const UNDISCOVERED_WALLET = {}; + const UNDISCOVERED_DEFAULT = {}; + const UNDISCOVERED_ALT = {}; + + UNDISCOVERED_WALLET.initialBalance = applyDelta(INIT_REGISTERED_BALANCE, { + // we sent TRANSFER + tx: 1, + confirmed: -HARD_FEE, + unconfirmed: -HARD_FEE + }); + + UNDISCOVERED_DEFAULT.initialBalance = UNDISCOVERED_WALLET.initialBalance; + UNDISCOVERED_ALT.initialBalance = NULL_BALANCE; + + UNDISCOVERED_WALLET.sentBalance = applyDelta(UNDISCOVERED_WALLET.initialBalance, { + tx: 1, + // default sent to alt. + coin: 0, + + unconfirmed: -HARD_FEE + }); + + UNDISCOVERED_DEFAULT.sentBalance = applyDelta(UNDISCOVERED_DEFAULT.initialBalance, { + tx: 1, + coin: -1, + unconfirmed: -FINAL_PRICE_1 - HARD_FEE, + ulocked: -FINAL_PRICE_1 + }); + + UNDISCOVERED_ALT.sentBalance = applyDelta(UNDISCOVERED_ALT.initialBalance, { + tx: 1, + coin: 1, + + unconfirmed: FINAL_PRICE_1, + ulocked: FINAL_PRICE_1 + }); + + UNDISCOVERED_WALLET.confirmedBalance = applyDelta(UNDISCOVERED_WALLET.sentBalance, { + confirmed: -HARD_FEE + }); + + UNDISCOVERED_DEFAULT.confirmedBalance = applyDelta(UNDISCOVERED_DEFAULT.sentBalance, { + confirmed: -FINAL_PRICE_1 - HARD_FEE, + clocked: -FINAL_PRICE_1 + }); + + UNDISCOVERED_ALT.confirmedBalance = applyDelta(UNDISCOVERED_ALT.sentBalance, { + confirmed: FINAL_PRICE_1, + clocked: FINAL_PRICE_1 + }); + + UNDISCOVERED_WALLET.unconfirmedBalance = UNDISCOVERED_WALLET.sentBalance; + UNDISCOVERED_WALLET.eraseBalance = UNDISCOVERED_WALLET.initialBalance; + UNDISCOVERED_WALLET.blockConfirmedBalance = UNDISCOVERED_WALLET.confirmedBalance; + UNDISCOVERED_WALLET.blockUnconfirmedBalance = UNDISCOVERED_WALLET.unconfirmedBalance; + + UNDISCOVERED_DEFAULT.unconfirmedBalance = UNDISCOVERED_DEFAULT.sentBalance; + UNDISCOVERED_DEFAULT.eraseBalance = UNDISCOVERED_DEFAULT.initialBalance; + UNDISCOVERED_DEFAULT.blockConfirmedBalance = UNDISCOVERED_DEFAULT.confirmedBalance; + UNDISCOVERED_DEFAULT.blockUnconfirmedBalance = UNDISCOVERED_DEFAULT.unconfirmedBalance; + + UNDISCOVERED_ALT.unconfirmedBalance = UNDISCOVERED_ALT.sentBalance; + UNDISCOVERED_ALT.eraseBalance = UNDISCOVERED_ALT.initialBalance; + UNDISCOVERED_ALT.blockConfirmedBalance = UNDISCOVERED_ALT.confirmedBalance; + UNDISCOVERED_ALT.blockUnconfirmedBalance = UNDISCOVERED_ALT.unconfirmedBalance; + + const DISCOVERED_WALLET = {}; + const DISCOVERED_DEFAULT = {}; + const DISCOVERED_ALT = {}; + + DISCOVERED_WALLET.initialBalance = UNDISCOVERED_WALLET.initialBalance; + DISCOVERED_DEFAULT.initialBalance = UNDISCOVERED_DEFAULT.initialBalance; + DISCOVERED_ALT.initialBalance = UNDISCOVERED_ALT.initialBalance; + + DISCOVERED_WALLET.sentBalance = applyDelta(DISCOVERED_WALLET.initialBalance, { + tx: 1, + // we discover receiving finalize + coin: 1, + + unconfirmed: -HARD_FEE + FINAL_PRICE_2, + ulocked: FINAL_PRICE_2 + }); + + DISCOVERED_DEFAULT.sentBalance = applyDelta(DISCOVERED_DEFAULT.initialBalance, { + tx: 1, + coin: -1, + + unconfirmed: -FINAL_PRICE_1 - HARD_FEE, + ulocked: -FINAL_PRICE_1 + }); + + DISCOVERED_ALT.sentBalance = applyDelta(DISCOVERED_ALT.initialBalance, { + tx: 1, + coin: 2, + + unconfirmed: FINAL_PRICE_1 + FINAL_PRICE_2, + ulocked: FINAL_PRICE_1 + FINAL_PRICE_2 + }); + + DISCOVERED_WALLET.confirmedBalance = applyDelta(DISCOVERED_WALLET.sentBalance, { + confirmed: -HARD_FEE + FINAL_PRICE_2, + clocked: FINAL_PRICE_2 + }); + + DISCOVERED_DEFAULT.confirmedBalance = applyDelta(DISCOVERED_DEFAULT.sentBalance, { + confirmed: -FINAL_PRICE_1 - HARD_FEE, + clocked: -FINAL_PRICE_1 + }); + + DISCOVERED_ALT.confirmedBalance = applyDelta(DISCOVERED_ALT.sentBalance, { + confirmed: FINAL_PRICE_1 + FINAL_PRICE_2, + clocked: FINAL_PRICE_1 + FINAL_PRICE_2 + }); + + DISCOVERED_WALLET.unconfirmedBalance = DISCOVERED_WALLET.sentBalance; + DISCOVERED_WALLET.eraseBalance = DISCOVERED_WALLET.initialBalance; + DISCOVERED_WALLET.blockConfirmedBalance = DISCOVERED_WALLET.confirmedBalance; + DISCOVERED_WALLET.blockUnconfirmedBalance = DISCOVERED_WALLET.unconfirmedBalance; + + DISCOVERED_DEFAULT.unconfirmedBalance = DISCOVERED_DEFAULT.sentBalance; + DISCOVERED_DEFAULT.eraseBalance = DISCOVERED_DEFAULT.initialBalance; + DISCOVERED_DEFAULT.blockConfirmedBalance = DISCOVERED_DEFAULT.confirmedBalance; + DISCOVERED_DEFAULT.blockUnconfirmedBalance = DISCOVERED_DEFAULT.unconfirmedBalance; + + DISCOVERED_ALT.unconfirmedBalance = DISCOVERED_ALT.sentBalance; + DISCOVERED_ALT.eraseBalance = DISCOVERED_ALT.initialBalance; + DISCOVERED_ALT.blockConfirmedBalance = DISCOVERED_ALT.confirmedBalance; + DISCOVERED_ALT.blockUnconfirmedBalance = DISCOVERED_ALT.unconfirmedBalance; + + genTests({ + name: 'should send finalize (cross acct)', + undiscovered: [UNDISCOVERED_WALLET, UNDISCOVERED_DEFAULT, UNDISCOVERED_ALT], + discovered: [DISCOVERED_WALLET, DISCOVERED_DEFAULT, DISCOVERED_ALT], + tester: testSendFinalizes, + discoverer: altDiscover + }); + }); +}); diff --git a/test/wallet-migration-test.js b/test/wallet-migration-test.js index c5be2e605..f25e1e995 100644 --- a/test/wallet-migration-test.js +++ b/test/wallet-migration-test.js @@ -2,9 +2,14 @@ const assert = require('bsert'); const fs = require('bfile'); +const random = require('bcrypto/lib/random'); const Network = require('../lib/protocol/network'); +const rules = require('../lib/covenants/rules'); +const Coin = require('../lib/primitives/coin'); const WalletDB = require('../lib/wallet/walletdb'); const layouts = require('../lib/wallet/layout'); +const TXDB = require('../lib/wallet/txdb'); +const {Credit} = TXDB; const WalletMigrator = require('../lib/wallet/migrations'); const {MigrateMigrations} = require('../lib/wallet/migrations'); const MigrationState = require('../lib/migrations/state'); @@ -236,7 +241,7 @@ describe('Wallet Migrations', function() { const walletOptions = { prefix: location, memory: false, - network: network + network }; let walletDB, ldb, wdbOpenSync; @@ -706,6 +711,254 @@ describe('Wallet Migrations', function() { await walletDB.close(); }); }); + + describe('Migrate txdb (integration)', function() { + const location = testdir('walet-txdb-refresh'); + const migrationsBAK = WalletMigrator.migrations; + + const walletOptions = { + prefix: location, + memory: false, + network + }; + + const balanceEquals = (balance, expected) => { + assert.strictEqual(balance.tx, expected.tx); + assert.strictEqual(balance.coin, expected.coin); + assert.strictEqual(balance.unconfirmed, expected.unconfirmed); + assert.strictEqual(balance.confirmed, expected.confirmed); + assert.strictEqual(balance.ulocked, expected.ulocked); + assert.strictEqual(balance.clocked, expected.clocked); + }; + + let walletDB, ldb, wdbOpenSync; + before(async () => { + WalletMigrator.migrations = {}; + await fs.mkdirp(location); + }); + + after(async () => { + WalletMigrator.migrations = migrationsBAK; + await rimraf(location); + }); + + beforeEach(async () => { + walletDB = new WalletDB(walletOptions); + ldb = walletDB.db; + wdbOpenSync = wdbOpenSyncFn(walletDB); + }); + + afterEach(async () => { + if (ldb.opened) + await ldb.close(); + }); + + it('should write some coins w/o updating balance', async () => { + // generate credits for the first 10 addresses stored on initialization. + await wdbOpenSync(); + + const wallet = walletDB.primary; + + await wallet.createAccount({ + name: 'alt' + }); + + const randomCoin = (options) => { + const coin = new Coin({ + version: 1, + coinbase: false, + hash: random.randomBytes(32), + index: 0, + ...options + }); + + if (options.covenantType != null) + coin.covenant.type = options.covenantType; + + return coin; + }; + + const coins = []; + const spentCoins = []; + + const addCoin = (addr, spent, confirmed, bid) => { + const list = spent ? spentCoins : coins; + + const coin = randomCoin({ + value: 1e6, + address: addr.getAddress(), + height: confirmed ? 1 : -1 + }); + + if (bid) + coin.covenant.type = rules.types.BID; + + list.push(coin); + }; + + for (let i = 0; i < 5; i++) { + const addr0 = await wallet.createReceive(0); + const addr1 = await wallet.createReceive(1); + + // 5 NONE coins to default account, of each type: + // confirmed spent, + // unconfirmed spent, + // unconfirmed unspent, + // confirmed unspent + + // confirmed += 1e6 * 5; + // unconfirmed += 1e6 * 5; + // coin += 5; + addCoin(addr0, false, true); + + // confirmed += 1e6 * 5; + // unconfirmed += 0; + // coin += 0; + addCoin(addr0, true, true); + + // confirmed += 0; + // unconfirmed += 0; + // coin += 0; + addCoin(addr0, true, false); + + // confirmed += 0; + // unconfirmed += 1e6 * 5; + // coin += 5; + addCoin(addr0, false, false); + + // 5 BID coins to alt account, of each type: + // confirmed spent, + // unconfirmed spent, + // unconfirmed unspent, + // confirmed unspent + + // confirmed += 1e6 * 5; + // unconfirmed += 1e6 * 5; + // coin += 5; + // locked += 1e6 * 5; + // unlocked += 1e6 * 5; + addCoin(addr1, false, true, true); + + // confirmed += 1e6 * 5; + // unconfirmed += 0; + // coin += 0; + // locked += 1e6 * 5; + // unlocked += 0; + addCoin(addr1, true, true, true); + + // confirmed += 0; + // unconfirmed += 0; + // coin += 0; + // locked += 0; + // unlocked += 0; + addCoin(addr1, true, false, true); + + // confirmed += 0; + // unconfirmed += 1e6 * 5; + // coin += 5; + // locked += 0; + // unlocked += 1e6 * 5; + addCoin(addr1, false, false, true); + } + + const batch = wallet.txdb.bucket.batch(); + for (const coin of coins) { + const path = await wallet.txdb.getPath(coin); + const credit = new Credit(coin); + await wallet.txdb.saveCredit(batch, credit, path); + } + + for (const coin of spentCoins) { + const path = await wallet.txdb.getPath(coin); + const credit = new Credit(coin, true); + await wallet.txdb.saveCredit(batch, credit, path); + } + + await batch.write(); + + await walletDB.close(); + }); + + it('should have incorrect balance before migration', async () => { + await wdbOpenSync(); + + const wallet = walletDB.primary; + const balance = await wallet.getBalance(-1); + const defBalance = await wallet.getBalance(0); + const altBalance = await wallet.getBalance(1); + + const empty = { + tx: 0, + coin: 0, + unconfirmed: 0, + confirmed: 0, + ulocked: 0, + clocked: 0 + }; + + balanceEquals(balance, empty); + balanceEquals(defBalance, empty); + balanceEquals(altBalance, empty); + + await walletDB.close(); + }); + + it('should enable txdb migration', () => { + WalletMigrator.migrations = { + 0: WalletMigrator.MigrateTXDBBalances + }; + }); + + it('should migrate', async () => { + walletDB.options.walletMigrate = 0; + + await wdbOpenSync(); + + const wallet = walletDB.primary; + const balance = await wallet.getBalance(-1); + const defBalance = await wallet.getBalance(0); + const altBalance = await wallet.getBalance(1); + + const expectedDefault = { + tx: 0, + coin: 10, + + confirmed: 10e6, + unconfirmed: 10e6, + + ulocked: 0, + clocked: 0 + }; + + const expectedAlt = { + tx: 0, + coin: 10, + + confirmed: 10e6, + unconfirmed: 10e6, + + ulocked: 10e6, + clocked: 10e6 + }; + + const expecteBalance = { + tx: expectedDefault.tx + expectedAlt.tx, + coin: expectedDefault.coin + expectedAlt.coin, + + confirmed: expectedDefault.confirmed + expectedAlt.confirmed, + unconfirmed: expectedDefault.unconfirmed + expectedAlt.unconfirmed, + + ulocked: expectedDefault.ulocked + expectedAlt.ulocked, + clocked: expectedDefault.clocked + expectedAlt.clocked + }; + + balanceEquals(defBalance, expectedDefault); + balanceEquals(altBalance, expectedAlt); + balanceEquals(balance, expecteBalance); + + await walletDB.close(); + }); + }); }); function writeVersion(b, name, version) { diff --git a/test/wallet-test.js b/test/wallet-test.js index e6108a7db..da47e0445 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -227,6 +227,10 @@ describe('Wallet', function() { await wdb.addTX(t4.toTX()); const balance = await alice.getBalance(); + // UCoins: + // t4:0 - 11k + // t4:1 - 11k + assert.strictEqual(balance.coin, 2); assert.strictEqual(balance.unconfirmed, 22000); } @@ -234,28 +238,56 @@ describe('Wallet', function() { await wdb.addTX(t1.toTX()); const balance = await alice.getBalance(); + // UCoins: + // t1:0 - 50k + // t1:1 - 1k + // t4:0 - 11k + // t4:1 - 11k + assert.strictEqual(balance.coin, 4); + // 22000 + 51000 = 73000 assert.strictEqual(balance.unconfirmed, 73000); } { await wdb.addTX(t2.toTX()); + // t2 spends 50k from t1, but adds 48k from t2 + // 2k less = 71000. BUT t2 output is consumed by t4 so: + // -24k. 71000 - 24000 = 47000 + // UCoins: + // t1:0 - 1k (t1:1 - 50k gone) + // t2:0 - 24k (t2:1 is already consumed by t4) + // t4:0 - 11k + // t4:1 - 11k const balance = await alice.getBalance(); - assert.strictEqual(balance.unconfirmed, 71000); + assert.strictEqual(balance.coin, 4); + assert.strictEqual(balance.unconfirmed, 47000); } { await wdb.addTX(t3.toTX()); + // UCoins consumed: + // t1:1 - 1k + // t2:0 - 24k + // t3:0 - 23k - already spent by t4 + // UCoins: + // t4:0 - 11k + // t4:1 - 11k const balance = await alice.getBalance(); - assert.strictEqual(balance.unconfirmed, 69000); + assert.strictEqual(balance.coin, 2); + assert.strictEqual(balance.unconfirmed, 22000); } { await wdb.addTX(f1.toTX()); + // Coins consumed: + // t4:1 - 11k + // Coins: + // t4:0 - 11k const balance = await alice.getBalance(); - assert.strictEqual(balance.unconfirmed, 58000); + assert.strictEqual(balance.unconfirmed, 11000); const txs = await alice.getHistory(); assert(txs.some((wtx) => { @@ -535,34 +567,67 @@ describe('Wallet', function() { await alice.sign(f1); { + // Coins: + // t4:0 - 11k + // t4:1 - 11k await wdb.addTX(t4.toTX()); const balance = await alice.getBalance(); + assert.strictEqual(balance.coin, 2); assert.strictEqual(balance.unconfirmed, 22000); } { + // Coins: + // t1:0 - 50k + // t1:1 - 1k + // t4:0 - 11k + // t4:1 - 11k await wdb.addTX(t1.toTX()); const balance = await alice.getBalance(); + assert.strictEqual(balance.coin, 4); assert.strictEqual(balance.unconfirmed, 73000); } { + // Coins consumed: + // t1:0 - 50k + // Coins already spent: + // t2:1 - 24k + // Coins: + // t1:1 - 1k + // t2:0 - 24k + // t4:0 - 11k + // t4:1 - 11k await wdb.addTX(t2.toTX()); const balance = await alice.getBalance(); - assert.strictEqual(balance.unconfirmed, 71000); + assert.strictEqual(balance.coin, 4); + assert.strictEqual(balance.unconfirmed, 47000); } { + // Coins consumed: + // t1:1 - 1k + // t2:0 - 24k + // Coins already spent: + // t3:0 - 23k + // Coins: + // t4:0 - 11k + // t4:1 - 11k await wdb.addTX(t3.toTX()); const balance = await alice.getBalance(); - assert.strictEqual(balance.unconfirmed, 69000); + assert.strictEqual(balance.coin, 2); + assert.strictEqual(balance.unconfirmed, 22000); } { await wdb.addTX(f1.toTX()); + // Coins consumed (alice) + // t4:1 - 11k + // Coins: + // t4:0 - 11k const balance = await alice.getBalance(); - assert.strictEqual(balance.unconfirmed, 58000); + assert.strictEqual(balance.unconfirmed, 11000); const txs = await alice.getHistory(); assert(txs.some((wtx) => { @@ -2356,7 +2421,7 @@ describe('Wallet', function() { uTXCount++; // Check - const senderBal3 = await wallet.getBalance(); + const senderBal3 = await wallet.getBalance(); assert.strictEqual(senderBal3.tx, 7); // One less wallet coin because name UTXO belongs to recip now assert.strictEqual(senderBal3.coin, 3); @@ -3345,7 +3410,7 @@ describe('Wallet', function() { assert.strictEqual(bal.ulocked, value); assert.strictEqual(bal.clocked, value + secondHighest); - // Confirm REGISTER + // Confirm REDEEM const block = { height: wdb.height + 1, hash: Buffer.alloc(32),