wallet: track account balances.

This commit is contained in:
Christopher Jeffrey 2017-10-20 03:37:59 -07:00
parent 175f169504
commit fe261db706
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
8 changed files with 263 additions and 310 deletions

View File

@ -857,7 +857,7 @@ Account.prototype.inspect = function inspect() {
* @returns {Object} * @returns {Object}
*/ */
Account.prototype.toJSON = function toJSON(minimal) { Account.prototype.toJSON = function toJSON(minimal, balance) {
const receive = this.receiveAddress(); const receive = this.receiveAddress();
const change = this.changeAddress(); const change = this.changeAddress();
const nested = this.nestedAddress(); const nested = this.nestedAddress();
@ -881,7 +881,8 @@ Account.prototype.toJSON = function toJSON(minimal) {
changeAddress: change ? change.toString() : null, changeAddress: change ? change.toString() : null,
nestedAddress: nested ? nested.toString() : null, nestedAddress: nested ? nested.toString() : null,
accountKey: this.accountKey.toBase58(), accountKey: this.accountKey.toBase58(),
keys: this.keys.map(key => key.toBase58()) keys: this.keys.map(key => key.toBase58()),
balance: balance ? balance.toJSON(true) : null
}; };
}; };

View File

@ -191,8 +191,9 @@ HTTPServer.prototype.initRouter = function initRouter() {
}); });
// Get wallet // Get wallet
this.get('/:id', (req, res) => { this.get('/:id', async (req, res) => {
res.send(200, req.wallet.toJSON()); const balance = await req.wallet.getBalance();
res.send(200, req.wallet.toJSON(false, balance));
}); });
// Get wallet master key // Get wallet master key
@ -217,7 +218,9 @@ HTTPServer.prototype.initRouter = function initRouter() {
watchOnly: valid.bool('watchOnly') watchOnly: valid.bool('watchOnly')
}); });
res.send(200, wallet.toJSON()); const balance = await wallet.getBalance();
res.send(200, wallet.toJSON(false, balance));
}); });
// Create wallet // Create wallet
@ -237,7 +240,9 @@ HTTPServer.prototype.initRouter = function initRouter() {
watchOnly: valid.bool('watchOnly') watchOnly: valid.bool('watchOnly')
}); });
res.send(200, wallet.toJSON()); const balance = await wallet.getBalance();
res.send(200, wallet.toJSON(false, balance));
}); });
// List accounts // List accounts
@ -257,7 +262,9 @@ HTTPServer.prototype.initRouter = function initRouter() {
return; return;
} }
res.send(200, account.toJSON()); const balance = await req.wallet.getBalance(account.accountIndex);
res.send(200, account.toJSON(false, balance));
}); });
// Create account (compat) // Create account (compat)
@ -277,8 +284,9 @@ HTTPServer.prototype.initRouter = function initRouter() {
}; };
const account = await req.wallet.createAccount(options, passphrase); 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 // Create account
@ -298,8 +306,9 @@ HTTPServer.prototype.initRouter = function initRouter() {
}; };
const account = await req.wallet.createAccount(options, passphrase); 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 // Change passphrase

View File

@ -99,6 +99,14 @@ layouts.txdb = {
return 't' + pad32(wid); return 't' + pad32(wid);
}, },
R: 'R', 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) { hi: function hi(ch, hash, index) {
assert(typeof hash === 'string'); assert(typeof hash === 'string');
return ch + hash + pad32(index); return ch + hash + pad32(index);

View File

@ -207,6 +207,18 @@ layouts.txdb = {
return out; return out;
}, },
R: Buffer.from([0x52]), 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) { hi: function hi(ch, hash, index) {
assert(typeof hash === 'string'); assert(typeof hash === 'string');
assert(typeof index === 'number'); assert(typeof index === 'number');

View File

@ -651,7 +651,7 @@ RPC.prototype.getWalletInfo = async function getWalletInfo(args, help) {
walletversion: 6, walletversion: 6,
balance: Amount.btc(balance.unconfirmed, true), balance: Amount.btc(balance.unconfirmed, true),
unconfirmed_balance: Amount.btc(balance.unconfirmed, true), unconfirmed_balance: Amount.btc(balance.unconfirmed, true),
txcount: wallet.txdb.state.tx, txcount: balance.tx,
keypoololdest: 0, keypoololdest: 0,
keypoolsize: 0, keypoolsize: 0,
unlocked_until: wallet.master.until, unlocked_until: wallet.master.until,

View File

@ -42,11 +42,7 @@ function TXDB(wdb) {
this.id = null; this.id = null;
this.prefix = layout.prefix(0); this.prefix = layout.prefix(0);
this.wallet = null; this.wallet = null;
this.locked = new Set(); 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) { TXDB.prototype.open = async function open(wallet) {
const {wid, id} = wallet; const {wid, id} = wallet;
this.id = id; this.id = id;
this.wid = wid; this.wid = wid;
this.prefix = layout.prefix(wid); this.prefix = layout.prefix(wid);
this.wallet = wallet; 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); 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. * Test a whether a coin has been spent.
* @param {Hash} hash * @param {Hash} hash
@ -493,7 +495,7 @@ TXDB.prototype.add = async function add(tx, block) {
return null; return null;
// Confirm transaction. // Confirm transaction.
return await this.confirm(existing, block); return this.confirm(existing, block);
} }
const wtx = TXRecord.fromTX(tx, 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. // 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) { TXDB.prototype.insert = async function insert(wtx, block) {
const b = this.bucket(); const b = this.bucket();
const state = this.state.clone();
const {tx, hash} = wtx; const {tx, hash} = wtx;
const height = block ? block.height : -1; const height = block ? block.height : -1;
const details = new Details(this, wtx, block); const details = new Details(this, wtx, block);
const accounts = new Set(); const state = new BalanceDelta();
let own = false; let own = false;
let updated = false;
if (!tx.isCoinbase()) { if (!tx.isCoinbase()) {
// We need to potentially spend some coins here. // 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 // Build the tx details object
// as we go, for speed. // as we go, for speed.
details.setInput(i, path, coin); details.setInput(i, path, coin);
accounts.add(path.account);
// Write an undo coin for the credit // Write an undo coin for the credit
// and add it to the stxo set. // 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 // be updated as it reflects the on-chain
// balance _and_ mempool balance assuming // balance _and_ mempool balance assuming
// everything in the mempool were to confirm. // everything in the mempool were to confirm.
state.coin -= 1; state.tx(path, 1);
state.unconfirmed -= coin.value; state.coin(path, -1);
state.unconfirmed(path, -coin.value);
if (!block) { if (!block) {
// If the tx is not mined, we do not // 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 will be indexed as an undo
// coin so it can be reconnected // coin so it can be reconnected
// later during a reorg. // later during a reorg.
state.confirmed -= coin.value; state.confirmed(path, -coin.value);
await this.removeCredit(b, credit, path); await this.removeCredit(b, credit, path);
} }
updated = true;
own = true; own = true;
} }
} }
@ -601,25 +600,23 @@ TXDB.prototype.insert = async function insert(wtx, block) {
continue; continue;
details.setOutput(i, path); details.setOutput(i, path);
accounts.add(path.account);
const credit = Credit.fromTX(tx, i, height); const credit = Credit.fromTX(tx, i, height);
credit.own = own; credit.own = own;
state.coin += 1; state.tx(path, 1);
state.unconfirmed += output.value; state.coin(path, 1);
state.unconfirmed(path, output.value);
if (block) if (block)
state.confirmed += output.value; state.confirmed(path, output.value);
await this.saveCredit(b, credit, path); await this.saveCredit(b, credit, path);
updated = true;
} }
// If this didn't update any coins, // If this didn't update any coins,
// it's not our transaction. // it's not our transaction.
if (!updated) if (!state.updated())
return null; return null;
// Save and index the transaction record. // 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 // Do some secondary indexing for account-based
// queries. This saves us a lot of time for // queries. This saves us a lot of time for
// queries later. // queries later.
for (const account of accounts) { for (const [acct, delta] of state.accounts) {
b.put(layout.T(account, hash), null); await this.updateAccountBalance(b, acct, delta);
b.put(layout.M(account, wtx.mtime, hash), null);
b.put(layout.T(acct, hash), null);
b.put(layout.M(acct, wtx.mtime, hash), null);
if (!block) if (!block)
b.put(layout.P(account, hash), null); b.put(layout.P(acct, hash), null);
else 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. // Update block records.
if (block) { if (block) {
await this.addBlockMap(b, height); await this.addBlockMap(b, height);
await this.addBlock(b, tx.hash(), block); await this.addBlock(b, tx.hash(), block);
} else {
await this.addTXMap(b, hash);
} }
// Update the transaction counter and // Commit the new state.
// commit the new state. This state will const balance = await this.updateBalance(b, state);
// only overwrite the best state once
// the batch has actually been written await b.write();
// to disk.
state.tx += 1;
b.put(layout.R, state.commit());
// This transaction may unlock some // This transaction may unlock some
// coins now that we've seen it. // coins now that we've seen it.
this.unlockTX(tx); this.unlockTX(tx);
await b.write();
this.state = state;
// Emit events for potential local and // Emit events for potential local and
// websocket listeners. Note that these // websocket listeners. Note that these
// will only be emitted if the batch is // will only be emitted if the batch is
// successfully written to disk. // successfully written to disk.
this.emit('tx', tx, details); this.emit('tx', tx, details);
this.emit('balance', state.toBalance(), details); this.emit('balance', balance);
return details; return details;
}; };
@ -688,12 +680,10 @@ TXDB.prototype.insert = async function insert(wtx, block) {
TXDB.prototype.confirm = async function confirm(wtx, block) { TXDB.prototype.confirm = async function confirm(wtx, block) {
const b = this.bucket(); const b = this.bucket();
const state = this.state.clone(); const {tx, hash} = wtx;
const tx = wtx.tx;
const hash = wtx.hash;
const height = block.height; const height = block.height;
const details = new Details(this, wtx, block); const details = new Details(this, wtx, block);
const accounts = new Set(); const state = new BalanceDelta();
wtx.setBlock(block); wtx.setBlock(block);
@ -719,6 +709,9 @@ TXDB.prototype.confirm = async function confirm(wtx, block) {
if (!credit) if (!credit)
continue; continue;
const path = await this.getPath(credit.coin);
assert(path);
// Add a spend record and undo coin // Add a spend record and undo coin
// for the coin we now know is ours. // for the coin we now know is ours.
// We don't need to remove the coin // We don't need to remove the coin
@ -726,8 +719,8 @@ TXDB.prototype.confirm = async function confirm(wtx, block) {
// first place. // first place.
this.spendCredit(b, credit, tx, i); this.spendCredit(b, credit, tx, i);
state.coin -= 1; state.coin(path, -1);
state.unconfirmed -= credit.coin.value; state.unconfirmed(path, -credit.coin.value);
} }
const coin = credit.coin; const coin = credit.coin;
@ -738,12 +731,11 @@ TXDB.prototype.confirm = async function confirm(wtx, block) {
assert(path); assert(path);
details.setInput(i, path, coin); details.setInput(i, path, coin);
accounts.add(path.account);
// We can now safely remove the credit // We can now safely remove the credit
// entirely, now that we know it's also // entirely, now that we know it's also
// been removed on-chain. // been removed on-chain.
state.confirmed -= coin.value; state.confirmed(path, -coin.value);
await this.removeCredit(b, credit, path); await this.removeCredit(b, credit, path);
} }
@ -758,7 +750,6 @@ TXDB.prototype.confirm = async function confirm(wtx, block) {
continue; continue;
details.setOutput(i, path); details.setOutput(i, path);
accounts.add(path.account);
const credit = await this.getCredit(hash, i); const credit = await this.getCredit(hash, i);
assert(credit); assert(credit);
@ -772,7 +763,7 @@ TXDB.prototype.confirm = async function confirm(wtx, block) {
// Update coin height and confirmed // Update coin height and confirmed
// balance. Save once again. // balance. Save once again.
state.confirmed += output.value; state.confirmed(path, output.value);
credit.coin.height = height; credit.coin.height = height;
await this.saveCredit(b, credit, path); 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); b.put(layout.h(height, hash), null);
// Secondary indexing also needs to change. // Secondary indexing also needs to change.
for (const account of accounts) { for (const [acct, delta] of state.accounts) {
b.del(layout.P(account, hash)); await this.updateAccountBalance(b, acct, delta);
b.put(layout.H(account, height, hash), null); 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.addBlockMap(b, height);
await this.addBlock(b, tx.hash(), block); await this.addBlock(b, tx.hash(), block);
// Commit the new state. The balance has updated. // Commit the new state. The balance has updated.
b.put(layout.R, state.commit()); const balance = await this.updateBalance(b, state);
await b.write(); await b.write();
this.state = state;
this.unlockTX(tx); this.unlockTX(tx);
this.emit('confirmed', tx, details); this.emit('confirmed', tx, details);
this.emit('balance', state.toBalance(), details); this.emit('balance', balance);
return details; return details;
}; };
@ -822,7 +813,7 @@ TXDB.prototype.remove = async function remove(hash) {
if (!wtx) if (!wtx)
return null; 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) { TXDB.prototype.erase = async function erase(wtx, block) {
const b = this.bucket(); const b = this.bucket();
const state = this.state.clone();
const {tx, hash} = wtx; const {tx, hash} = wtx;
const height = block ? block.height : -1; const height = block ? block.height : -1;
const details = new Details(this, wtx, block); const details = new Details(this, wtx, block);
const accounts = new Set(); const state = new BalanceDelta();
if (!tx.isCoinbase()) { if (!tx.isCoinbase()) {
// We need to undo every part of the // We need to undo every part of the
@ -861,18 +851,20 @@ TXDB.prototype.erase = async function erase(wtx, block) {
assert(path); assert(path);
details.setInput(i, path, coin); details.setInput(i, path, coin);
accounts.add(path.account);
// Recalculate the balance, remove // Recalculate the balance, remove
// from stxo set, remove the undo // from stxo set, remove the undo
// coin, and resave the credit. // coin, and resave the credit.
state.coin += 1; state.tx(path, -1);
state.unconfirmed += coin.value; state.coin(path, 1);
state.unconfirmed(path, coin.value);
if (block) if (block)
state.confirmed += coin.value; state.confirmed(path, coin.value);
this.unspendCredit(b, tx, i); this.unspendCredit(b, tx, i);
credit.spent = false;
await this.saveCredit(b, credit, path); await this.saveCredit(b, credit, path);
} }
} }
@ -887,15 +879,15 @@ TXDB.prototype.erase = async function erase(wtx, block) {
continue; continue;
details.setOutput(i, path); details.setOutput(i, path);
accounts.add(path.account);
const credit = Credit.fromTX(tx, i, height); const credit = Credit.fromTX(tx, i, height);
state.coin -= 1; state.tx(path, -1);
state.unconfirmed -= output.value; state.coin(path, -1);
state.unconfirmed(path, -output.value);
if (block) if (block)
state.confirmed -= output.value; state.confirmed(path, -output.value);
await this.removeCredit(b, credit, path); await this.removeCredit(b, credit, path);
} }
@ -911,36 +903,35 @@ TXDB.prototype.erase = async function erase(wtx, block) {
b.del(layout.h(height, hash)); b.del(layout.h(height, hash));
// Remove all secondary indexing. // Remove all secondary indexing.
for (const account of accounts) { for (const [acct, delta] of state.accounts) {
b.del(layout.T(account, hash)); await this.updateAccountBalance(b, acct, delta);
b.del(layout.M(account, wtx.mtime, hash));
b.del(layout.T(acct, hash));
b.del(layout.M(acct, wtx.mtime, hash));
if (!block) if (!block)
b.del(layout.P(account, hash)); b.del(layout.P(acct, hash));
else else
b.del(layout.H(account, height, hash)); b.del(layout.H(acct, height, hash));
} }
await this.removeTXMap(b, hash);
// Update block records. // Update block records.
if (block) { if (block) {
await this.removeBlockMap(b, height); await this.removeBlockMap(b, height);
await this.spliceBlock(b, hash, height); await this.spliceBlock(b, hash, height);
} else {
await this.removeTXMap(b, hash);
} }
// Update the transaction counter // Update the transaction counter
// and commit new state due to // and commit new state due to
// balance change. // balance change.
state.tx -= 1; const balance = await this.updateBalance(b, state);
b.put(layout.R, state.commit());
await b.write(); await b.write();
this.state = state;
this.emit('remove tx', tx, details); this.emit('remove tx', tx, details);
this.emit('balance', state.toBalance(), details); this.emit('balance', balance);
return details; return details;
}; };
@ -954,8 +945,7 @@ TXDB.prototype.erase = async function erase(wtx, block) {
*/ */
TXDB.prototype.removeRecursive = async function removeRecursive(wtx) { TXDB.prototype.removeRecursive = async function removeRecursive(wtx) {
const tx = wtx.tx; const {tx, hash} = wtx;
const hash = wtx.hash;
for (let i = 0; i < tx.outputs.length; i++) { for (let i = 0; i < tx.outputs.length; i++) {
const spent = await this.getSpent(hash, i); const spent = await this.getSpent(hash, i);
@ -972,11 +962,7 @@ TXDB.prototype.removeRecursive = async function removeRecursive(wtx) {
} }
// Remove the spender. // Remove the spender.
const details = await this.erase(wtx, wtx.getBlock()); return this.erase(wtx, wtx.getBlock());
assert(details);
return details;
}; };
/** /**
@ -1017,7 +1003,7 @@ TXDB.prototype.unconfirm = async function unconfirm(hash) {
if (wtx.height === -1) if (wtx.height === -1)
return null; 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) { TXDB.prototype.disconnect = async function disconnect(wtx, block) {
const b = this.bucket(); const b = this.bucket();
const state = this.state.clone();
const {tx, hash, height} = wtx; const {tx, hash, height} = wtx;
const details = new Details(this, wtx, block); const details = new Details(this, wtx, block);
const accounts = new Set(); const state = new BalanceDelta();
assert(block); assert(block);
@ -1059,9 +1044,8 @@ TXDB.prototype.disconnect = async function disconnect(wtx, block) {
assert(path); assert(path);
details.setInput(i, path, coin); details.setInput(i, path, coin);
accounts.add(path.account);
state.confirmed += coin.value; state.confirmed(path, coin.value);
// Resave the credit and mark it // Resave the credit and mark it
// as spent in the mempool instead. // 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); await this.updateSpentCoin(b, tx, i, height);
details.setOutput(i, path); details.setOutput(i, path);
accounts.add(path.account);
// Update coin height and confirmed // Update coin height and confirmed
// balance. Save once again. // balance. Save once again.
const coin = credit.coin; credit.coin.height = -1;
coin.height = -1;
state.confirmed -= output.value; state.confirmed(path, -output.value);
await this.saveCredit(b, credit, path); await this.saveCredit(b, credit, path);
} }
await this.addTXMap(b, hash);
await this.removeBlockMap(b, height); await this.removeBlockMap(b, height);
await this.removeBlock(b, tx.hash(), 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)); b.del(layout.h(height, hash));
// Secondary indexing also needs to change. // Secondary indexing also needs to change.
for (const account of accounts) { for (const [acct, delta] of state.accounts) {
b.put(layout.P(account, hash), null); await this.updateAccountBalance(b, acct, delta);
b.del(layout.H(account, height, hash)); b.put(layout.P(acct, hash), null);
b.del(layout.H(acct, height, hash));
} }
// Commit state due to unconfirmed // Commit state due to unconfirmed
// vs. confirmed balance change. // vs. confirmed balance change.
b.put(layout.R, state.commit()); const balance = await this.updateBalance(b, state);
await b.write(); await b.write();
this.state = state;
this.emit('unconfirmed', tx, details); this.emit('unconfirmed', tx, details);
this.emit('balance', state.toBalance(), details); this.emit('balance', balance);
return details; return details;
}; };
@ -1304,6 +1286,7 @@ TXDB.prototype.getLocked = function getLocked() {
*/ */
TXDB.prototype.getAccountHistoryHashes = function getAccountHistoryHashes(acct) { TXDB.prototype.getAccountHistoryHashes = function getAccountHistoryHashes(acct) {
assert(typeof acct === 'number');
return this.keys({ return this.keys({
gte: layout.T(acct, encoding.NULL_HASH), gte: layout.T(acct, encoding.NULL_HASH),
lte: layout.T(acct, encoding.HIGH_HASH), lte: layout.T(acct, encoding.HIGH_HASH),
@ -1340,6 +1323,7 @@ TXDB.prototype.getHistoryHashes = function getHistoryHashes(acct) {
*/ */
TXDB.prototype.getAccountPendingHashes = function getAccountPendingHashes(acct) { TXDB.prototype.getAccountPendingHashes = function getAccountPendingHashes(acct) {
assert(typeof acct === 'number');
return this.keys({ return this.keys({
gte: layout.P(acct, encoding.NULL_HASH), gte: layout.P(acct, encoding.NULL_HASH),
lte: layout.P(acct, encoding.HIGH_HASH), lte: layout.P(acct, encoding.HIGH_HASH),
@ -1376,6 +1360,7 @@ TXDB.prototype.getPendingHashes = function getPendingHashes(acct) {
*/ */
TXDB.prototype.getAccountOutpoints = function getAccountOutpoints(acct) { TXDB.prototype.getAccountOutpoints = function getAccountOutpoints(acct) {
assert(typeof acct === 'number');
return this.keys({ return this.keys({
gte: layout.C(acct, encoding.NULL_HASH, 0), gte: layout.C(acct, encoding.NULL_HASH, 0),
lte: layout.C(acct, encoding.HIGH_HASH, 0xffffffff), 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) { TXDB.prototype.getAccountHeightRangeHashes = function getAccountHeightRangeHashes(acct, options) {
assert(typeof acct === 'number');
const start = options.start || 0; const start = options.start || 0;
const end = options.end || 0xffffffff; const end = options.end || 0xffffffff;
@ -1489,6 +1476,8 @@ TXDB.prototype.getHeightHashes = function getHeightHashes(height) {
*/ */
TXDB.prototype.getAccountRangeHashes = function getAccountRangeHashes(acct, options) { TXDB.prototype.getAccountRangeHashes = function getAccountRangeHashes(acct, options) {
assert(typeof acct === 'number');
const start = options.start || 0; const start = options.start || 0;
const end = options.end || 0xffffffff; const end = options.end || 0xffffffff;
@ -1828,20 +1817,6 @@ TXDB.prototype.getSpentView = async function getSpentView(tx) {
return view; 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. * Get transaction.
* @param {Hash} hash * @param {Hash} hash
@ -1869,7 +1844,7 @@ TXDB.prototype.getDetails = async function getDetails(hash) {
if (!wtx) if (!wtx)
return null; return null;
return await this.toDetails(wtx); return this.toDetails(wtx);
}; };
/** /**
@ -1882,7 +1857,7 @@ TXDB.prototype.toDetails = async function toDetails(wtxs) {
const out = []; const out = [];
if (!Array.isArray(wtxs)) if (!Array.isArray(wtxs))
return await this._toDetails(wtxs); return this._toDetails(wtxs);
for (const wtx of wtxs) { for (const wtx of wtxs) {
const details = await this._toDetails(wtx); 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) { 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) { TXDB.prototype.getBalance = async function getBalance(acct) {
assert(typeof acct === 'number'); assert(typeof acct === 'number');
// Slow case
if (acct !== -1) if (acct !== -1)
return await this.getAccountBalance(acct); return this.getAccountBalance(acct);
// Fast case return this.getWalletBalance();
return this.state.toBalance();
}; };
/** /**
@ -2062,20 +2035,12 @@ TXDB.prototype.getBalance = async function getBalance(acct) {
*/ */
TXDB.prototype.getWalletBalance = async function getWalletBalance() { TXDB.prototype.getWalletBalance = async function getWalletBalance() {
const credits = await this.getCredits(); const data = await this.get(layout.R);
const balance = new Balance(this.wid, this.id, -1);
for (const credit of credits) { if (!data)
const coin = credit.coin; return new Balance();
if (coin.height !== -1) return Balance.fromRaw(-1, data);
balance.confirmed += coin.value;
if (!credit.spent)
balance.unconfirmed += coin.value;
}
return balance;
}; };
/** /**
@ -2085,20 +2050,12 @@ TXDB.prototype.getWalletBalance = async function getWalletBalance() {
*/ */
TXDB.prototype.getAccountBalance = async function getAccountBalance(acct) { TXDB.prototype.getAccountBalance = async function getAccountBalance(acct) {
const credits = await this.getAccountCredits(acct); const data = await this.get(layout.r(acct));
const balance = new Balance(this.wid, this.id, acct);
for (const credit of credits) { if (!data)
const coin = credit.coin; return new Balance(acct);
if (coin.height !== -1) return Balance.fromRaw(acct, data);
balance.confirmed += coin.value;
if (!credit.spent)
balance.unconfirmed += coin.value;
}
return balance;
}; };
/** /**
@ -2149,138 +2106,47 @@ TXDB.prototype.abandon = async function abandon(hash) {
if (!result) if (!result)
throw new Error('TX not eligible.'); throw new Error('TX not eligible.');
return await this.remove(hash); return this.remove(hash);
}; };
/** /**
* Balance * Balance
* @alias module:wallet.Balance * @alias module:wallet.Balance
* @constructor * @constructor
* @param {WalletID} wid
* @param {String} id
* @param {Number} account * @param {Number} account
*/ */
function Balance(wid, id, account) { function Balance(acct = -1) {
if (!(this instanceof Balance)) if (!(this instanceof Balance))
return new Balance(wid, id, account); return new Balance(acct);
this.wid = wid; assert(typeof acct === 'number');
this.id = id;
this.account = account;
this.unconfirmed = 0;
this.confirmed = 0;
}
/** this.account = acct;
* 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 '<Balance'
+ ` unconfirmed=${Amount.btc(this.unconfirmed)}`
+ ` confirmed=${Amount.btc(this.confirmed)}`
+ '>';
};
/**
* 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.tx = 0; this.tx = 0;
this.coin = 0; this.coin = 0;
this.unconfirmed = 0; this.unconfirmed = 0;
this.confirmed = 0; this.confirmed = 0;
this.committed = false;
} }
/** /**
* Clone the state. * Apply delta.
* @returns {TXDBState} * @param {Balance} balance
*/ */
TXDBState.prototype.clone = function clone() { Balance.prototype.applyTo = function applyTo(balance) {
const state = new TXDBState(this.wid, this.id); balance.tx += this.tx;
state.tx = this.tx; balance.coin += this.coin;
state.coin = this.coin; balance.unconfirmed += this.unconfirmed;
state.unconfirmed = this.unconfirmed; balance.confirmed += this.confirmed;
state.confirmed = this.confirmed;
return state;
}; };
/** /**
* Commit and serialize state. * Serialize balance.
* @returns {Buffer} * @returns {Buffer}
*/ */
TXDBState.prototype.commit = function commit() { Balance.prototype.toRaw = function toRaw() {
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() {
const bw = new StaticWriter(32); const bw = new StaticWriter(32);
bw.writeU64(this.tx); bw.writeU64(this.tx);
@ -2298,7 +2164,7 @@ TXDBState.prototype.toRaw = function toRaw() {
* @returns {TXDBState} * @returns {TXDBState}
*/ */
TXDBState.prototype.fromRaw = function fromRaw(data) { Balance.prototype.fromRaw = function fromRaw(data) {
const br = new BufferReader(data); const br = new BufferReader(data);
this.tx = br.readU64(); this.tx = br.readU64();
this.coin = 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 * @param {Buffer} data
* @returns {TXDBState} * @returns {TXDBState}
*/ */
TXDBState.fromRaw = function fromRaw(wid, id, data) { Balance.fromRaw = function fromRaw(acct, data) {
return new TXDBState(wid, id).fromRaw(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 * @param {Boolean?} minimal
* @returns {Object} * @returns {Object}
*/ */
TXDBState.prototype.toJSON = function toJSON(minimal) { Balance.prototype.toJSON = function toJSON(minimal) {
return { return {
wid: !minimal ? this.wid : undefined, account: !minimal ? this.account : undefined,
id: !minimal ? this.id : undefined,
tx: this.tx, tx: this.tx,
coin: this.coin, coin: this.coin,
unconfirmed: this.unconfirmed, unconfirmed: this.unconfirmed,
@ -2335,12 +2201,68 @@ TXDBState.prototype.toJSON = function toJSON(minimal) {
}; };
/** /**
* Inspect the state. * Inspect balance.
* @returns {Object} * @param {String}
*/ */
TXDBState.prototype.inspect = function inspect() { Balance.prototype.inspect = function inspect() {
return this.toJSON(); return this;
return '<Balance'
+ ` tx=${this.tx}`
+ ` coin=${this.coin}`
+ ` unconfirmed=${Amount.btc(this.unconfirmed)}`
+ ` confirmed=${Amount.btc(this.confirmed)}`
+ '>';
};
/**
* 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;
}; };
/** /**

View File

@ -2279,7 +2279,6 @@ Wallet.prototype.inspect = function inspect() {
accountDepth: this.accountDepth, accountDepth: this.accountDepth,
token: this.token.toString('hex'), token: this.token.toString('hex'),
tokenDepth: this.tokenDepth, tokenDepth: this.tokenDepth,
state: this.txdb.state ? this.txdb.state.toJSON(true) : null,
master: this.master master: this.master
}; };
}; };
@ -2292,7 +2291,7 @@ Wallet.prototype.inspect = function inspect() {
* @returns {Object} * @returns {Object}
*/ */
Wallet.prototype.toJSON = function toJSON(unsafe) { Wallet.prototype.toJSON = function toJSON(unsafe, balance) {
return { return {
network: this.network.type, network: this.network.type,
wid: this.wid, wid: this.wid,
@ -2302,8 +2301,8 @@ Wallet.prototype.toJSON = function toJSON(unsafe) {
accountDepth: this.accountDepth, accountDepth: this.accountDepth,
token: this.token.toString('hex'), token: this.token.toString('hex'),
tokenDepth: this.tokenDepth, 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
}; };
}; };

View File

@ -1380,8 +1380,7 @@ describe('Wallet', function() {
t1.addInput(dummyInput()); t1.addInput(dummyInput());
t1.addOutput(addr, 50000); t1.addOutput(addr, 50000);
await alice.add(t1.toTX()); await wdb.addTX(t1.toTX());
await bob.add(t1.toTX());
// Bob misses this tx! // Bob misses this tx!
const t2 = new MTX(); const t2 = new MTX();
@ -1407,16 +1406,18 @@ describe('Wallet', function() {
assert.strictEqual((await bob.getBalance()).unconfirmed, 50000); assert.strictEqual((await bob.getBalance()).unconfirmed, 50000);
await alice.add(t3.toTX()); await wdb.addTX(t3.toTX());
await bob.add(t3.toTX());
assert.strictEqual((await alice.getBalance()).unconfirmed, 30000); assert.strictEqual((await alice.getBalance()).unconfirmed, 30000);
// t1 gets confirmed.
await wdb.addBlock(nextBlock(wdb), [t1.toTX()]);
// Bob sees t2 on the chain. // 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. // 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); assert.strictEqual((await bob.getBalance()).unconfirmed, 30000);
}); });
@ -1440,8 +1441,7 @@ describe('Wallet', function() {
t1.addInput(dummyInput()); t1.addInput(dummyInput());
t1.addOutput(addr, 50000); t1.addOutput(addr, 50000);
await alice.add(t1.toTX()); await wdb.addTX(t1.toTX());
await bob.add(t1.toTX());
// Bob misses this tx! // Bob misses this tx!
const t2a = new MTX(); const t2a = new MTX();
@ -1477,16 +1477,18 @@ describe('Wallet', function() {
assert.strictEqual((await bob.getBalance()).unconfirmed, 20000); assert.strictEqual((await bob.getBalance()).unconfirmed, 20000);
await alice.add(t3.toTX()); await wdb.addTX(t3.toTX());
await bob.add(t3.toTX());
assert.strictEqual((await alice.getBalance()).unconfirmed, 30000); assert.strictEqual((await alice.getBalance()).unconfirmed, 30000);
// t1 gets confirmed.
await wdb.addBlock(nextBlock(wdb), [t1.toTX()]);
// Bob sees t2a on the chain. // 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. // 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); assert.strictEqual((await bob.getBalance()).unconfirmed, 30000);
}); });