fix(balance): stop the displayed balance cratering during a pending shielded send
Sending a small amount from an address holding a large balance made the displayed balance collapse to ~0 until the tx confirmed. A shielded spend consumes the whole source note; the change returns as a 0-confirmation note, and every balance query used the default minconf=1 — so the spent note dropped out and the change wasn't counted yet. Split every balance into two views: - DISPLAY (balance / privateBalance / transparentBalance / totalBalance) — now queried at minconf=0, so it INCLUDES the user's own pending change and no longer craters. This is what the Overview, balance tab, market portfolio and receive tab show. unconfirmedBalance is now populated (= total - spendable). - SPENDABLE (new spendableBalance / spendable*Balance) — confirmed (minconf>=1), what z_sendmany (run at minconf=1) can actually spend. The Send form's available/ Max/validation, the from-address selection, the drag-to-transfer dialog cap, the chat pay-from and the auto-shield gate all size off these, so they never offer 0-conf change the daemon would reject. Implementation: a single z_listunspent(0)/listunspent(0), partitioned per-note by "confirmations">=1; z_gettotalbalance called at minconf 0 (display) and 1 (spendable); the z_getbalance fallback queries both. applyPendingSendDelta (the optimistic post-send debit) now touches ONLY the spendable fields — debiting the display too would re-crater it on top of the honest minconf=0 RPC. Lite mirrors spendableBalance = balance (its per-address balance is already confirmed) so lite sends aren't zeroed. The confirmed-only gates (seed-migration/sweep z_gettotalbalance, sweep z_getbalance(addr,1), z_sendmany's minconf arg) are untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1012,10 +1012,13 @@ void App::applyPendingSendDelta(const std::string& fromAddress, double signedAmo
|
||||
// For a debit (signedAmount < 0) this clamps at >=0 exactly as the debit sites did. For a
|
||||
// restore (signedAmount > 0) the clamp is a no-op — max(0, bal+amt) == bal+amt when bal,amt>=0 —
|
||||
// so the single clamped form reproduces the original restore's unclamped bal += amt.
|
||||
// Debit/restore the SPENDABLE view ONLY. The DISPLAY balances now come from an honest minconf=0 RPC
|
||||
// that already reflects the outgoing spend AND the incoming 0-conf change, so debiting them here too
|
||||
// would re-crater the display. This delta only lowers what can be re-spent before the change confirms.
|
||||
auto applyToAddress = [&](std::vector<AddressInfo>& addresses) {
|
||||
for (auto& address : addresses) {
|
||||
if (address.address == fromAddress) {
|
||||
address.balance = std::max(0.0, address.balance + signedAmount);
|
||||
address.spendableBalance = std::max(0.0, address.spendableBalance + signedAmount);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1024,11 +1027,12 @@ void App::applyPendingSendDelta(const std::string& fromAddress, double signedAmo
|
||||
if (!applyToAddress(state_.z_addresses)) applyToAddress(state_.t_addresses);
|
||||
if (includeAggregates) {
|
||||
if (!fromAddress.empty() && fromAddress[0] == 'z') {
|
||||
state_.privateBalance = std::max(0.0, state_.privateBalance + signedAmount);
|
||||
state_.spendablePrivateBalance = std::max(0.0, state_.spendablePrivateBalance + signedAmount);
|
||||
} else {
|
||||
state_.transparentBalance = std::max(0.0, state_.transparentBalance + signedAmount);
|
||||
state_.spendableTransparentBalance = std::max(0.0, state_.spendableTransparentBalance + signedAmount);
|
||||
}
|
||||
state_.totalBalance = std::max(0.0, state_.totalBalance + signedAmount);
|
||||
state_.spendableTotalBalance = std::max(0.0, state_.spendableTotalBalance + signedAmount);
|
||||
state_.unconfirmedBalance = std::max(0.0, state_.totalBalance - state_.spendableTotalBalance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1685,7 +1689,7 @@ void App::refreshCoreData()
|
||||
|
||||
// Auto-shield transparent funds if enabled
|
||||
if (result.balanceOk && settings_ && settings_->getAutoShield() &&
|
||||
state_.transparent_balance > 0.0001 && !state_.sync.syncing &&
|
||||
state_.spendableTransparentBalance > 0.0001 && !state_.sync.syncing &&
|
||||
!auto_shield_pending_.exchange(true)) {
|
||||
std::string targetZAddr;
|
||||
for (const auto& addr : state_.addresses) {
|
||||
@@ -1696,7 +1700,7 @@ void App::refreshCoreData()
|
||||
}
|
||||
if (!targetZAddr.empty() && worker_) {
|
||||
DEBUG_LOGF("[AutoShield] Shielding %.8f DRGX to %s\n",
|
||||
state_.transparent_balance, targetZAddr.c_str());
|
||||
state_.spendableTransparentBalance, targetZAddr.c_str());
|
||||
// Use the user-configured fee, formatted fixed-decimal so the daemon's
|
||||
// ParseFixedPoint accepts it (a small double would serialize to "5e-05").
|
||||
const std::string feeStr =
|
||||
@@ -3046,14 +3050,14 @@ std::string App::chatPayFromZaddr(double fee) const
|
||||
std::string reply;
|
||||
if (settings_) reply = settings_->getChatReplyZaddr();
|
||||
for (const auto& a : state_.z_addresses)
|
||||
if (a.address == reply && a.has_spending_key && a.balance >= fee) return reply;
|
||||
if (a.address == reply && a.has_spending_key && a.spendableBalance >= fee) return reply;
|
||||
|
||||
std::string best;
|
||||
double bestBal = -1.0;
|
||||
for (const auto& a : state_.z_addresses)
|
||||
if (a.has_spending_key && !a.address.empty() && a.balance >= fee && a.balance > bestBal) {
|
||||
if (a.has_spending_key && !a.address.empty() && a.spendableBalance >= fee && a.spendableBalance > bestBal) {
|
||||
best = a.address;
|
||||
bestBal = a.balance;
|
||||
bestBal = a.spendableBalance;
|
||||
}
|
||||
return best; // empty → no z-address can cover the fee
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user