Files
ObsidianDragon/src/data/wallet_state.cpp
T
DanSandClaude Opus 4.8 ea26c0cbbb 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>
2026-08-13 16:40:14 -05:00

80 lines
2.4 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "wallet_state.h"
#include "../util/text_format.h" // util::formatClockDateTime (app-wide 24h/12h clock)
#include <algorithm>
#include <ctime>
#include <sstream>
#include <iomanip>
namespace dragonx {
std::vector<size_t> sortedSpendableAddressIndices(const std::vector<AddressInfo>& addresses,
bool requirePositiveBalance)
{
std::vector<size_t> indices;
indices.reserve(addresses.size());
for (size_t i = 0; i < addresses.size(); ++i) {
const auto& address = addresses[i];
if (!address.isSpendable()) continue;
// Rank/filter by the CONFIRMED balance — an address holding only 0-conf change can't be sent from.
if (requirePositiveBalance && address.spendableBalance <= 0.0) continue;
indices.push_back(i);
}
std::sort(indices.begin(), indices.end(), [&](size_t lhs, size_t rhs) {
return addresses[lhs].spendableBalance > addresses[rhs].spendableBalance;
});
return indices;
}
int bestSpendableAddressIndex(const std::vector<AddressInfo>& addresses)
{
int bestIndex = -1;
double bestBalance = 0.0;
for (size_t i = 0; i < addresses.size(); ++i) {
if (addresses[i].isSpendable() && addresses[i].spendableBalance > bestBalance) {
bestBalance = addresses[i].spendableBalance;
bestIndex = static_cast<int>(i);
}
}
return bestIndex;
}
std::string TransactionInfo::getTimeString() const
{
if (timestamp == 0) return "Unknown";
return util::formatClockDateTime(timestamp);
}
std::string TransactionInfo::getTypeDisplay() const
{
if (type == "send") return "Sent";
if (type == "receive") return "Received";
if (type == "mined" || type == "generate" || type == "immature") return "Mined";
return type;
}
std::string PeerInfo::getConnectionTime() const
{
if (conntime == 0) return "Unknown";
int64_t now = std::time(nullptr);
int64_t diff = now - conntime;
if (diff < 60) return std::to_string(diff) + "s";
if (diff < 3600) return std::to_string(diff / 60) + "m";
if (diff < 86400) return std::to_string(diff / 3600) + "h";
return std::to_string(diff / 86400) + "d";
}
std::string BannedPeer::getBannedUntilString() const
{
if (banned_until == 0) return "Never";
return util::formatClockDateTime(banned_until);
}
} // namespace dragonx