diff --git a/lib/wallet/account.js b/lib/wallet/account.js index d01bfea4..fff6f238 100644 --- a/lib/wallet/account.js +++ b/lib/wallet/account.js @@ -857,7 +857,7 @@ Account.prototype.inspect = function inspect() { * @returns {Object} */ -Account.prototype.toJSON = function toJSON(minimal) { +Account.prototype.toJSON = function toJSON(minimal, balance) { const receive = this.receiveAddress(); const change = this.changeAddress(); const nested = this.nestedAddress(); @@ -881,7 +881,8 @@ Account.prototype.toJSON = function toJSON(minimal) { changeAddress: change ? change.toString() : null, nestedAddress: nested ? nested.toString() : null, accountKey: this.accountKey.toBase58(), - keys: this.keys.map(key => key.toBase58()) + keys: this.keys.map(key => key.toBase58()), + balance: balance ? balance.toJSON(true) : null }; }; diff --git a/lib/wallet/http.js b/lib/wallet/http.js index 70adb1fd..c62bb8bb 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -191,8 +191,9 @@ HTTPServer.prototype.initRouter = function initRouter() { }); // Get wallet - this.get('/:id', (req, res) => { - res.send(200, req.wallet.toJSON()); + this.get('/:id', async (req, res) => { + const balance = await req.wallet.getBalance(); + res.send(200, req.wallet.toJSON(false, balance)); }); // Get wallet master key @@ -217,7 +218,9 @@ HTTPServer.prototype.initRouter = function initRouter() { watchOnly: valid.bool('watchOnly') }); - res.send(200, wallet.toJSON()); + const balance = await wallet.getBalance(); + + res.send(200, wallet.toJSON(false, balance)); }); // Create wallet @@ -237,7 +240,9 @@ HTTPServer.prototype.initRouter = function initRouter() { watchOnly: valid.bool('watchOnly') }); - res.send(200, wallet.toJSON()); + const balance = await wallet.getBalance(); + + res.send(200, wallet.toJSON(false, balance)); }); // List accounts @@ -257,7 +262,9 @@ HTTPServer.prototype.initRouter = function initRouter() { return; } - res.send(200, account.toJSON()); + const balance = await req.wallet.getBalance(account.accountIndex); + + res.send(200, account.toJSON(false, balance)); }); // Create account (compat) @@ -277,8 +284,9 @@ HTTPServer.prototype.initRouter = function initRouter() { }; const account = await req.wallet.createAccount(options, passphrase); + const balance = await req.wallet.getBalance(account.accountIndex); - res.send(200, account.toJSON()); + res.send(200, account.toJSON(false, balance)); }); // Create account @@ -298,8 +306,9 @@ HTTPServer.prototype.initRouter = function initRouter() { }; const account = await req.wallet.createAccount(options, passphrase); + const balance = await req.wallet.getBalance(account.accountIndex); - res.send(200, account.toJSON()); + res.send(200, account.toJSON(false, balance)); }); // Change passphrase diff --git a/lib/wallet/layout-browser.js b/lib/wallet/layout-browser.js index dd285dce..4615ef0e 100644 --- a/lib/wallet/layout-browser.js +++ b/lib/wallet/layout-browser.js @@ -99,6 +99,14 @@ layouts.txdb = { return 't' + pad32(wid); }, R: 'R', + r: function r(acct) { + assert(typeof acct === 'number'); + return 'r' + pad32(acct); + }, + rr: function rr(key) { + assert(typeof key === 'string'); + return parseInt(key.slice(1), 10); + }, hi: function hi(ch, hash, index) { assert(typeof hash === 'string'); return ch + hash + pad32(index); diff --git a/lib/wallet/layout.js b/lib/wallet/layout.js index 84819b3e..434712c5 100644 --- a/lib/wallet/layout.js +++ b/lib/wallet/layout.js @@ -207,6 +207,18 @@ layouts.txdb = { return out; }, R: Buffer.from([0x52]), + r: function r(acct) { + assert(typeof acct === 'number'); + const key = Buffer.allocUnsafe(5); + key[0] = 0x72; + key.writeUInt32BE(acct, 1, true); + return key; + }, + rr: function rr(key) { + assert(Buffer.isBuffer(key)); + assert(key.length === 5); + return key.readUInt32BE(1, true); + }, hi: function hi(ch, hash, index) { assert(typeof hash === 'string'); assert(typeof index === 'number'); diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js index a88d58aa..95dd65cb 100644 --- a/lib/wallet/rpc.js +++ b/lib/wallet/rpc.js @@ -651,7 +651,7 @@ RPC.prototype.getWalletInfo = async function getWalletInfo(args, help) { walletversion: 6, balance: Amount.btc(balance.unconfirmed, true), unconfirmed_balance: Amount.btc(balance.unconfirmed, true), - txcount: wallet.txdb.state.tx, + txcount: balance.tx, keypoololdest: 0, keypoolsize: 0, unlocked_until: wallet.master.until, diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 92ec8777..1ed9cabf 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -42,11 +42,7 @@ function TXDB(wdb) { this.id = null; this.prefix = layout.prefix(0); this.wallet = null; - this.locked = new Set(); - this.state = null; - this.pending = null; - this.events = []; } /** @@ -63,29 +59,10 @@ TXDB.layout = layout; TXDB.prototype.open = async function open(wallet) { const {wid, id} = wallet; - this.id = id; this.wid = wid; this.prefix = layout.prefix(wid); this.wallet = wallet; - - const state = await this.getState(); - - if (state) { - this.state = state; - this.logger.info('TXDB loaded for %s.', id); - } else { - this.state = new TXDBState(wid, id); - this.logger.info('TXDB created for %s.', id); - } - - this.logger.info('TXDB State: tx=%d coin=%s.', - this.state.tx, this.state.coin); - - this.logger.info( - 'Balance: unconfirmed=%s confirmed=%s.', - Amount.btc(this.state.unconfirmed), - Amount.btc(this.state.confirmed)); }; /** @@ -270,6 +247,31 @@ TXDB.prototype.removeInput = async function removeInput(b, tx, index) { return this.removeOutpointMap(b, prevout.hash, prevout.index); }; +/** + * Update wallet balance. + * @param {BalanceDelta} state + */ + +TXDB.prototype.updateBalance = async function updateBalance(b, state) { + const balance = await this.getWalletBalance(); + state.applyTo(balance); + b.put(layout.R, balance.toRaw()); + return balance; +}; + +/** + * Update account balance. + * @param {Number} acct + * @param {Balance} delta + */ + +TXDB.prototype.updateAccountBalance = async function updateAccountBalance(b, acct, delta) { + const balance = await this.getAccountBalance(acct); + delta.applyTo(balance); + b.put(layout.r(acct), balance.toRaw()); + return balance; +}; + /** * Test a whether a coin has been spent. * @param {Hash} hash @@ -493,7 +495,7 @@ TXDB.prototype.add = async function add(tx, block) { return null; // Confirm transaction. - return await this.confirm(existing, block); + return this.confirm(existing, block); } const wtx = TXRecord.fromTX(tx, block); @@ -509,7 +511,7 @@ TXDB.prototype.add = async function add(tx, block) { } // Finally we can do a regular insertion. - return await this.insert(wtx, block); + return this.insert(wtx, block); }; /** @@ -522,14 +524,12 @@ TXDB.prototype.add = async function add(tx, block) { TXDB.prototype.insert = async function insert(wtx, block) { const b = this.bucket(); - const state = this.state.clone(); const {tx, hash} = wtx; const height = block ? block.height : -1; const details = new Details(this, wtx, block); - const accounts = new Set(); + const state = new BalanceDelta(); let own = false; - let updated = false; if (!tx.isCoinbase()) { // We need to potentially spend some coins here. @@ -553,7 +553,6 @@ TXDB.prototype.insert = async function insert(wtx, block) { // Build the tx details object // as we go, for speed. details.setInput(i, path, coin); - accounts.add(path.account); // Write an undo coin for the credit // and add it to the stxo set. @@ -563,8 +562,9 @@ TXDB.prototype.insert = async function insert(wtx, block) { // be updated as it reflects the on-chain // balance _and_ mempool balance assuming // everything in the mempool were to confirm. - state.coin -= 1; - state.unconfirmed -= coin.value; + state.tx(path, 1); + state.coin(path, -1); + state.unconfirmed(path, -coin.value); if (!block) { // If the tx is not mined, we do not @@ -583,11 +583,10 @@ TXDB.prototype.insert = async function insert(wtx, block) { // coin will be indexed as an undo // coin so it can be reconnected // later during a reorg. - state.confirmed -= coin.value; + state.confirmed(path, -coin.value); await this.removeCredit(b, credit, path); } - updated = true; own = true; } } @@ -601,25 +600,23 @@ TXDB.prototype.insert = async function insert(wtx, block) { continue; details.setOutput(i, path); - accounts.add(path.account); const credit = Credit.fromTX(tx, i, height); credit.own = own; - state.coin += 1; - state.unconfirmed += output.value; + state.tx(path, 1); + state.coin(path, 1); + state.unconfirmed(path, output.value); if (block) - state.confirmed += output.value; + state.confirmed(path, output.value); await this.saveCredit(b, credit, path); - - updated = true; } // If this didn't update any coins, // it's not our transaction. - if (!updated) + if (!state.updated()) return null; // Save and index the transaction record. @@ -634,46 +631,41 @@ TXDB.prototype.insert = async function insert(wtx, block) { // Do some secondary indexing for account-based // queries. This saves us a lot of time for // queries later. - for (const account of accounts) { - b.put(layout.T(account, hash), null); - b.put(layout.M(account, wtx.mtime, hash), null); + for (const [acct, delta] of state.accounts) { + await this.updateAccountBalance(b, acct, delta); + + b.put(layout.T(acct, hash), null); + b.put(layout.M(acct, wtx.mtime, hash), null); if (!block) - b.put(layout.P(account, hash), null); + b.put(layout.P(acct, hash), null); else - b.put(layout.H(account, height, hash), null); + b.put(layout.H(acct, height, hash), null); } - await this.addTXMap(b, hash); - // Update block records. if (block) { await this.addBlockMap(b, height); await this.addBlock(b, tx.hash(), block); + } else { + await this.addTXMap(b, hash); } - // Update the transaction counter and - // commit the new state. This state will - // only overwrite the best state once - // the batch has actually been written - // to disk. - state.tx += 1; - b.put(layout.R, state.commit()); + // Commit the new state. + const balance = await this.updateBalance(b, state); + + await b.write(); // This transaction may unlock some // coins now that we've seen it. this.unlockTX(tx); - await b.write(); - - this.state = state; - // Emit events for potential local and // websocket listeners. Note that these // will only be emitted if the batch is // successfully written to disk. this.emit('tx', tx, details); - this.emit('balance', state.toBalance(), details); + this.emit('balance', balance); return details; }; @@ -688,12 +680,10 @@ TXDB.prototype.insert = async function insert(wtx, block) { TXDB.prototype.confirm = async function confirm(wtx, block) { const b = this.bucket(); - const state = this.state.clone(); - const tx = wtx.tx; - const hash = wtx.hash; + const {tx, hash} = wtx; const height = block.height; const details = new Details(this, wtx, block); - const accounts = new Set(); + const state = new BalanceDelta(); wtx.setBlock(block); @@ -719,6 +709,9 @@ TXDB.prototype.confirm = async function confirm(wtx, block) { if (!credit) continue; + const path = await this.getPath(credit.coin); + assert(path); + // Add a spend record and undo coin // for the coin we now know is ours. // We don't need to remove the coin @@ -726,8 +719,8 @@ TXDB.prototype.confirm = async function confirm(wtx, block) { // first place. this.spendCredit(b, credit, tx, i); - state.coin -= 1; - state.unconfirmed -= credit.coin.value; + state.coin(path, -1); + state.unconfirmed(path, -credit.coin.value); } const coin = credit.coin; @@ -738,12 +731,11 @@ TXDB.prototype.confirm = async function confirm(wtx, block) { assert(path); details.setInput(i, path, coin); - accounts.add(path.account); // We can now safely remove the credit // entirely, now that we know it's also // been removed on-chain. - state.confirmed -= coin.value; + state.confirmed(path, -coin.value); await this.removeCredit(b, credit, path); } @@ -758,7 +750,6 @@ TXDB.prototype.confirm = async function confirm(wtx, block) { continue; details.setOutput(i, path); - accounts.add(path.account); const credit = await this.getCredit(hash, i); assert(credit); @@ -772,7 +763,7 @@ TXDB.prototype.confirm = async function confirm(wtx, block) { // Update coin height and confirmed // balance. Save once again. - state.confirmed += output.value; + state.confirmed(path, output.value); credit.coin.height = height; await this.saveCredit(b, credit, path); @@ -786,25 +777,25 @@ TXDB.prototype.confirm = async function confirm(wtx, block) { b.put(layout.h(height, hash), null); // Secondary indexing also needs to change. - for (const account of accounts) { - b.del(layout.P(account, hash)); - b.put(layout.H(account, height, hash), null); + for (const [acct, delta] of state.accounts) { + await this.updateAccountBalance(b, acct, delta); + b.del(layout.P(acct, hash)); + b.put(layout.H(acct, height, hash), null); } + await this.removeTXMap(b, hash); await this.addBlockMap(b, height); await this.addBlock(b, tx.hash(), block); // Commit the new state. The balance has updated. - b.put(layout.R, state.commit()); + const balance = await this.updateBalance(b, state); await b.write(); - this.state = state; - this.unlockTX(tx); this.emit('confirmed', tx, details); - this.emit('balance', state.toBalance(), details); + this.emit('balance', balance); return details; }; @@ -822,7 +813,7 @@ TXDB.prototype.remove = async function remove(hash) { if (!wtx) return null; - return await this.removeRecursive(wtx); + return this.removeRecursive(wtx); }; /** @@ -835,11 +826,10 @@ TXDB.prototype.remove = async function remove(hash) { TXDB.prototype.erase = async function erase(wtx, block) { const b = this.bucket(); - const state = this.state.clone(); const {tx, hash} = wtx; const height = block ? block.height : -1; const details = new Details(this, wtx, block); - const accounts = new Set(); + const state = new BalanceDelta(); if (!tx.isCoinbase()) { // We need to undo every part of the @@ -861,18 +851,20 @@ TXDB.prototype.erase = async function erase(wtx, block) { assert(path); details.setInput(i, path, coin); - accounts.add(path.account); // Recalculate the balance, remove // from stxo set, remove the undo // coin, and resave the credit. - state.coin += 1; - state.unconfirmed += coin.value; + state.tx(path, -1); + state.coin(path, 1); + state.unconfirmed(path, coin.value); if (block) - state.confirmed += coin.value; + state.confirmed(path, coin.value); this.unspendCredit(b, tx, i); + + credit.spent = false; await this.saveCredit(b, credit, path); } } @@ -887,15 +879,15 @@ TXDB.prototype.erase = async function erase(wtx, block) { continue; details.setOutput(i, path); - accounts.add(path.account); const credit = Credit.fromTX(tx, i, height); - state.coin -= 1; - state.unconfirmed -= output.value; + state.tx(path, -1); + state.coin(path, -1); + state.unconfirmed(path, -output.value); if (block) - state.confirmed -= output.value; + state.confirmed(path, -output.value); await this.removeCredit(b, credit, path); } @@ -911,36 +903,35 @@ TXDB.prototype.erase = async function erase(wtx, block) { b.del(layout.h(height, hash)); // Remove all secondary indexing. - for (const account of accounts) { - b.del(layout.T(account, hash)); - b.del(layout.M(account, wtx.mtime, hash)); + for (const [acct, delta] of state.accounts) { + await this.updateAccountBalance(b, acct, delta); + + b.del(layout.T(acct, hash)); + b.del(layout.M(acct, wtx.mtime, hash)); if (!block) - b.del(layout.P(account, hash)); + b.del(layout.P(acct, hash)); else - b.del(layout.H(account, height, hash)); + b.del(layout.H(acct, height, hash)); } - await this.removeTXMap(b, hash); - // Update block records. if (block) { await this.removeBlockMap(b, height); await this.spliceBlock(b, hash, height); + } else { + await this.removeTXMap(b, hash); } // Update the transaction counter // and commit new state due to // balance change. - state.tx -= 1; - b.put(layout.R, state.commit()); + const balance = await this.updateBalance(b, state); await b.write(); - this.state = state; - this.emit('remove tx', tx, details); - this.emit('balance', state.toBalance(), details); + this.emit('balance', balance); return details; }; @@ -954,8 +945,7 @@ TXDB.prototype.erase = async function erase(wtx, block) { */ TXDB.prototype.removeRecursive = async function removeRecursive(wtx) { - const tx = wtx.tx; - const hash = wtx.hash; + const {tx, hash} = wtx; for (let i = 0; i < tx.outputs.length; i++) { const spent = await this.getSpent(hash, i); @@ -972,11 +962,7 @@ TXDB.prototype.removeRecursive = async function removeRecursive(wtx) { } // Remove the spender. - const details = await this.erase(wtx, wtx.getBlock()); - - assert(details); - - return details; + return this.erase(wtx, wtx.getBlock()); }; /** @@ -1017,7 +1003,7 @@ TXDB.prototype.unconfirm = async function unconfirm(hash) { if (wtx.height === -1) return null; - return await this.disconnect(wtx, wtx.getBlock()); + return this.disconnect(wtx, wtx.getBlock()); }; /** @@ -1028,10 +1014,9 @@ TXDB.prototype.unconfirm = async function unconfirm(hash) { TXDB.prototype.disconnect = async function disconnect(wtx, block) { const b = this.bucket(); - const state = this.state.clone(); const {tx, hash, height} = wtx; const details = new Details(this, wtx, block); - const accounts = new Set(); + const state = new BalanceDelta(); assert(block); @@ -1059,9 +1044,8 @@ TXDB.prototype.disconnect = async function disconnect(wtx, block) { assert(path); details.setInput(i, path, coin); - accounts.add(path.account); - state.confirmed += coin.value; + state.confirmed(path, coin.value); // Resave the credit and mark it // as spent in the mempool instead. @@ -1091,18 +1075,17 @@ TXDB.prototype.disconnect = async function disconnect(wtx, block) { await this.updateSpentCoin(b, tx, i, height); details.setOutput(i, path); - accounts.add(path.account); // Update coin height and confirmed // balance. Save once again. - const coin = credit.coin; - coin.height = -1; + credit.coin.height = -1; - state.confirmed -= output.value; + state.confirmed(path, -output.value); await this.saveCredit(b, credit, path); } + await this.addTXMap(b, hash); await this.removeBlockMap(b, height); await this.removeBlock(b, tx.hash(), height); @@ -1114,21 +1097,20 @@ TXDB.prototype.disconnect = async function disconnect(wtx, block) { b.del(layout.h(height, hash)); // Secondary indexing also needs to change. - for (const account of accounts) { - b.put(layout.P(account, hash), null); - b.del(layout.H(account, height, hash)); + for (const [acct, delta] of state.accounts) { + await this.updateAccountBalance(b, acct, delta); + b.put(layout.P(acct, hash), null); + b.del(layout.H(acct, height, hash)); } // Commit state due to unconfirmed // vs. confirmed balance change. - b.put(layout.R, state.commit()); + const balance = await this.updateBalance(b, state); await b.write(); - this.state = state; - this.emit('unconfirmed', tx, details); - this.emit('balance', state.toBalance(), details); + this.emit('balance', balance); return details; }; @@ -1304,6 +1286,7 @@ TXDB.prototype.getLocked = function getLocked() { */ TXDB.prototype.getAccountHistoryHashes = function getAccountHistoryHashes(acct) { + assert(typeof acct === 'number'); return this.keys({ gte: layout.T(acct, encoding.NULL_HASH), lte: layout.T(acct, encoding.HIGH_HASH), @@ -1340,6 +1323,7 @@ TXDB.prototype.getHistoryHashes = function getHistoryHashes(acct) { */ TXDB.prototype.getAccountPendingHashes = function getAccountPendingHashes(acct) { + assert(typeof acct === 'number'); return this.keys({ gte: layout.P(acct, encoding.NULL_HASH), lte: layout.P(acct, encoding.HIGH_HASH), @@ -1376,6 +1360,7 @@ TXDB.prototype.getPendingHashes = function getPendingHashes(acct) { */ TXDB.prototype.getAccountOutpoints = function getAccountOutpoints(acct) { + assert(typeof acct === 'number'); return this.keys({ gte: layout.C(acct, encoding.NULL_HASH, 0), lte: layout.C(acct, encoding.HIGH_HASH, 0xffffffff), @@ -1420,6 +1405,8 @@ TXDB.prototype.getOutpoints = function getOutpoints(acct) { */ TXDB.prototype.getAccountHeightRangeHashes = function getAccountHeightRangeHashes(acct, options) { + assert(typeof acct === 'number'); + const start = options.start || 0; const end = options.end || 0xffffffff; @@ -1489,6 +1476,8 @@ TXDB.prototype.getHeightHashes = function getHeightHashes(height) { */ TXDB.prototype.getAccountRangeHashes = function getAccountRangeHashes(acct, options) { + assert(typeof acct === 'number'); + const start = options.start || 0; const end = options.end || 0xffffffff; @@ -1828,20 +1817,6 @@ TXDB.prototype.getSpentView = async function getSpentView(tx) { return view; }; -/** - * Get TXDB state. - * @returns {Promise} - */ - -TXDB.prototype.getState = async function getState() { - const data = await this.get(layout.R); - - if (!data) - return null; - - return TXDBState.fromRaw(this.wid, this.id, data); -}; - /** * Get transaction. * @param {Hash} hash @@ -1869,7 +1844,7 @@ TXDB.prototype.getDetails = async function getDetails(hash) { if (!wtx) return null; - return await this.toDetails(wtx); + return this.toDetails(wtx); }; /** @@ -1882,7 +1857,7 @@ TXDB.prototype.toDetails = async function toDetails(wtxs) { const out = []; if (!Array.isArray(wtxs)) - return await this._toDetails(wtxs); + return this._toDetails(wtxs); for (const wtx of wtxs) { const details = await this._toDetails(wtx); @@ -2036,7 +2011,7 @@ TXDB.prototype.updateSpentCoin = async function updateSpentCoin(b, tx, index, he */ TXDB.prototype.hasCoin = async function hasCoin(hash, index) { - return await this.has(layout.c(hash, index)); + return this.has(layout.c(hash, index)); }; /** @@ -2048,12 +2023,10 @@ TXDB.prototype.hasCoin = async function hasCoin(hash, index) { TXDB.prototype.getBalance = async function getBalance(acct) { assert(typeof acct === 'number'); - // Slow case if (acct !== -1) - return await this.getAccountBalance(acct); + return this.getAccountBalance(acct); - // Fast case - return this.state.toBalance(); + return this.getWalletBalance(); }; /** @@ -2062,20 +2035,12 @@ TXDB.prototype.getBalance = async function getBalance(acct) { */ TXDB.prototype.getWalletBalance = async function getWalletBalance() { - const credits = await this.getCredits(); - const balance = new Balance(this.wid, this.id, -1); + const data = await this.get(layout.R); - for (const credit of credits) { - const coin = credit.coin; + if (!data) + return new Balance(); - if (coin.height !== -1) - balance.confirmed += coin.value; - - if (!credit.spent) - balance.unconfirmed += coin.value; - } - - return balance; + return Balance.fromRaw(-1, data); }; /** @@ -2085,20 +2050,12 @@ TXDB.prototype.getWalletBalance = async function getWalletBalance() { */ TXDB.prototype.getAccountBalance = async function getAccountBalance(acct) { - const credits = await this.getAccountCredits(acct); - const balance = new Balance(this.wid, this.id, acct); + const data = await this.get(layout.r(acct)); - for (const credit of credits) { - const coin = credit.coin; + if (!data) + return new Balance(acct); - if (coin.height !== -1) - balance.confirmed += coin.value; - - if (!credit.spent) - balance.unconfirmed += coin.value; - } - - return balance; + return Balance.fromRaw(acct, data); }; /** @@ -2149,138 +2106,47 @@ TXDB.prototype.abandon = async function abandon(hash) { if (!result) throw new Error('TX not eligible.'); - return await this.remove(hash); + return this.remove(hash); }; /** * Balance * @alias module:wallet.Balance * @constructor - * @param {WalletID} wid - * @param {String} id * @param {Number} account */ -function Balance(wid, id, account) { +function Balance(acct = -1) { if (!(this instanceof Balance)) - return new Balance(wid, id, account); + return new Balance(acct); - this.wid = wid; - this.id = id; - this.account = account; - this.unconfirmed = 0; - this.confirmed = 0; -} + assert(typeof acct === 'number'); -/** - * Test whether a balance is equal. - * @param {Balance} balance - * @returns {Boolean} - */ - -Balance.prototype.equal = function equal(balance) { - return this.wid === balance.wid - && this.confirmed === balance.confirmed - && this.unconfirmed === balance.unconfirmed; -}; - -/** - * Convert balance to a more json-friendly object. - * @param {Boolean?} minimal - * @returns {Object} - */ - -Balance.prototype.toJSON = function toJSON(minimal) { - return { - wid: !minimal ? this.wid : undefined, - id: !minimal ? this.id : undefined, - account: !minimal ? this.account : undefined, - unconfirmed: this.unconfirmed, - confirmed: this.confirmed - }; -}; - -/** - * Convert balance to human-readable string. - * @returns {String} - */ - -Balance.prototype.toString = function toString() { - return ''; -}; - -/** - * Inspect balance. - * @param {String} - */ - -Balance.prototype.inspect = function inspect() { - return this.toString(); -}; - -/** - * Chain State - * @alias module:wallet.ChainState - * @constructor - * @param {WalletID} wid - * @param {String} id - */ - -function TXDBState(wid, id) { - this.wid = wid; - this.id = id; + this.account = acct; this.tx = 0; this.coin = 0; this.unconfirmed = 0; this.confirmed = 0; - this.committed = false; } /** - * Clone the state. - * @returns {TXDBState} + * Apply delta. + * @param {Balance} balance */ -TXDBState.prototype.clone = function clone() { - const state = new TXDBState(this.wid, this.id); - state.tx = this.tx; - state.coin = this.coin; - state.unconfirmed = this.unconfirmed; - state.confirmed = this.confirmed; - return state; +Balance.prototype.applyTo = function applyTo(balance) { + balance.tx += this.tx; + balance.coin += this.coin; + balance.unconfirmed += this.unconfirmed; + balance.confirmed += this.confirmed; }; /** - * Commit and serialize state. + * Serialize balance. * @returns {Buffer} */ -TXDBState.prototype.commit = function commit() { - this.committed = true; - return this.toRaw(); -}; - -/** - * Convert state to a balance object. - * @returns {Balance} - */ - -TXDBState.prototype.toBalance = function toBalance() { - const balance = new Balance(this.wid, this.id, -1); - balance.unconfirmed = this.unconfirmed; - balance.confirmed = this.confirmed; - return balance; -}; - -/** - * Serialize state. - * @returns {Buffer} - */ - -TXDBState.prototype.toRaw = function toRaw() { +Balance.prototype.toRaw = function toRaw() { const bw = new StaticWriter(32); bw.writeU64(this.tx); @@ -2298,7 +2164,7 @@ TXDBState.prototype.toRaw = function toRaw() { * @returns {TXDBState} */ -TXDBState.prototype.fromRaw = function fromRaw(data) { +Balance.prototype.fromRaw = function fromRaw(data) { const br = new BufferReader(data); this.tx = br.readU64(); this.coin = br.readU64(); @@ -2308,25 +2174,25 @@ TXDBState.prototype.fromRaw = function fromRaw(data) { }; /** - * Instantiate txdb state from serialized data. + * Instantiate balance from serialized data. + * @param {Number} acct * @param {Buffer} data * @returns {TXDBState} */ -TXDBState.fromRaw = function fromRaw(wid, id, data) { - return new TXDBState(wid, id).fromRaw(data); +Balance.fromRaw = function fromRaw(acct, data) { + return new Balance(acct).fromRaw(data); }; /** - * Convert state to a more json-friendly object. + * Convert balance to a more json-friendly object. * @param {Boolean?} minimal * @returns {Object} */ -TXDBState.prototype.toJSON = function toJSON(minimal) { +Balance.prototype.toJSON = function toJSON(minimal) { return { - wid: !minimal ? this.wid : undefined, - id: !minimal ? this.id : undefined, + account: !minimal ? this.account : undefined, tx: this.tx, coin: this.coin, unconfirmed: this.unconfirmed, @@ -2335,12 +2201,68 @@ TXDBState.prototype.toJSON = function toJSON(minimal) { }; /** - * Inspect the state. - * @returns {Object} + * Inspect balance. + * @param {String} */ -TXDBState.prototype.inspect = function inspect() { - return this.toJSON(); +Balance.prototype.inspect = function inspect() { + return this; + return ''; +}; + +/** + * Balance Delta + * @constructor + * @ignore + */ + +function BalanceDelta() { + this.wallet = new Balance(); + this.accounts = new Map(); +} + +BalanceDelta.prototype.updated = function updated() { + return this.wallet.tx !== 0; +}; + +BalanceDelta.prototype.applyTo = function applyTo(balance) { + this.wallet.applyTo(balance); +}; + +BalanceDelta.prototype.get = function get(path) { + if (!this.accounts.has(path.account)) + this.accounts.set(path.account, new Balance()); + + return this.accounts.get(path.account); +}; + +BalanceDelta.prototype.tx = function tx(path, value) { + const account = this.get(path); + account.tx = value; + this.wallet.tx = value; +}; + +BalanceDelta.prototype.coin = function coin(path, value) { + const account = this.get(path); + account.coin += value; + this.wallet.coin += value; +}; + +BalanceDelta.prototype.unconfirmed = function unconfirmed(path, value) { + const account = this.get(path); + account.unconfirmed += value; + this.wallet.unconfirmed += value; +}; + +BalanceDelta.prototype.confirmed = function confirmed(path, value) { + const account = this.get(path); + account.confirmed += value; + this.wallet.confirmed += value; }; /** diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 5e919262..37151746 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -2279,7 +2279,6 @@ Wallet.prototype.inspect = function inspect() { accountDepth: this.accountDepth, token: this.token.toString('hex'), tokenDepth: this.tokenDepth, - state: this.txdb.state ? this.txdb.state.toJSON(true) : null, master: this.master }; }; @@ -2292,7 +2291,7 @@ Wallet.prototype.inspect = function inspect() { * @returns {Object} */ -Wallet.prototype.toJSON = function toJSON(unsafe) { +Wallet.prototype.toJSON = function toJSON(unsafe, balance) { return { network: this.network.type, wid: this.wid, @@ -2302,8 +2301,8 @@ Wallet.prototype.toJSON = function toJSON(unsafe) { accountDepth: this.accountDepth, token: this.token.toString('hex'), tokenDepth: this.tokenDepth, - state: this.txdb.state.toJSON(true), - master: this.master.toJSON(unsafe) + master: this.master.toJSON(unsafe), + balance: balance ? balance.toJSON(true) : null }; }; diff --git a/test/wallet-test.js b/test/wallet-test.js index 07633f7c..6fd301db 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -1380,8 +1380,7 @@ describe('Wallet', function() { t1.addInput(dummyInput()); t1.addOutput(addr, 50000); - await alice.add(t1.toTX()); - await bob.add(t1.toTX()); + await wdb.addTX(t1.toTX()); // Bob misses this tx! const t2 = new MTX(); @@ -1407,16 +1406,18 @@ describe('Wallet', function() { assert.strictEqual((await bob.getBalance()).unconfirmed, 50000); - await alice.add(t3.toTX()); - await bob.add(t3.toTX()); + await wdb.addTX(t3.toTX()); assert.strictEqual((await alice.getBalance()).unconfirmed, 30000); + // t1 gets confirmed. + await wdb.addBlock(nextBlock(wdb), [t1.toTX()]); + // Bob sees t2 on the chain. - await bob.add(t2.toTX(), nextBlock(wdb)); + await wdb.addBlock(nextBlock(wdb), [t2.toTX()]); // Bob sees t3 on the chain. - await bob.add(t3.toTX(), nextBlock(wdb)); + await wdb.addBlock(nextBlock(wdb), [t3.toTX()]); assert.strictEqual((await bob.getBalance()).unconfirmed, 30000); }); @@ -1440,8 +1441,7 @@ describe('Wallet', function() { t1.addInput(dummyInput()); t1.addOutput(addr, 50000); - await alice.add(t1.toTX()); - await bob.add(t1.toTX()); + await wdb.addTX(t1.toTX()); // Bob misses this tx! const t2a = new MTX(); @@ -1477,16 +1477,18 @@ describe('Wallet', function() { assert.strictEqual((await bob.getBalance()).unconfirmed, 20000); - await alice.add(t3.toTX()); - await bob.add(t3.toTX()); + await wdb.addTX(t3.toTX()); assert.strictEqual((await alice.getBalance()).unconfirmed, 30000); + // t1 gets confirmed. + await wdb.addBlock(nextBlock(wdb), [t1.toTX()]); + // Bob sees t2a on the chain. - await bob.add(t2a.toTX(), nextBlock(wdb)); + await wdb.addBlock(nextBlock(wdb), [t2a.toTX()]); // Bob sees t3 on the chain. - await bob.add(t3.toTX(), nextBlock(wdb)); + await wdb.addBlock(nextBlock(wdb), [t3.toTX()]); assert.strictEqual((await bob.getBalance()).unconfirmed, 30000); });