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}
*/
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
};
};

View File

@ -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

View File

@ -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);

View File

@ -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');

View File

@ -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,

View File

@ -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 '<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.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'
+ ` 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,
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
};
};

View File

@ -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);
});