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
|
||||
}
|
||||
|
||||
@@ -170,10 +170,10 @@ void App::installDemoWalletData()
|
||||
}
|
||||
|
||||
auto zaddr = [](const char* a, double bal, const char* label) {
|
||||
AddressInfo i; i.address = a; i.balance = bal; i.type = "shielded"; i.label = label; return i;
|
||||
AddressInfo i; i.address = a; i.balance = bal; i.spendableBalance = bal; i.type = "shielded"; i.label = label; return i;
|
||||
};
|
||||
auto taddr = [](const char* a, double bal, const char* label) {
|
||||
AddressInfo i; i.address = a; i.balance = bal; i.type = "transparent"; i.label = label; return i;
|
||||
AddressInfo i; i.address = a; i.balance = bal; i.spendableBalance = bal; i.type = "transparent"; i.label = label; return i;
|
||||
};
|
||||
state_.z_addresses = {
|
||||
zaddr("zs1demoprimaryshieldedaddressforuisweep000000000000000000000000000", 12.0, "Savings"),
|
||||
|
||||
@@ -19,12 +19,13 @@ std::vector<size_t> sortedSpendableAddressIndices(const std::vector<AddressInfo>
|
||||
for (size_t i = 0; i < addresses.size(); ++i) {
|
||||
const auto& address = addresses[i];
|
||||
if (!address.isSpendable()) continue;
|
||||
if (requirePositiveBalance && address.balance <= 0.0) 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].balance > addresses[rhs].balance;
|
||||
return addresses[lhs].spendableBalance > addresses[rhs].spendableBalance;
|
||||
});
|
||||
return indices;
|
||||
}
|
||||
@@ -34,8 +35,8 @@ 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].balance > bestBalance) {
|
||||
bestBalance = addresses[i].balance;
|
||||
if (addresses[i].isSpendable() && addresses[i].spendableBalance > bestBalance) {
|
||||
bestBalance = addresses[i].spendableBalance;
|
||||
bestIndex = static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,17 @@ namespace dragonx {
|
||||
*/
|
||||
struct AddressInfo {
|
||||
std::string address;
|
||||
double balance = 0.0;
|
||||
double balance = 0.0; // DISPLAY total incl. pending 0-conf change (minconf=0)
|
||||
std::string type; // "shielded" or "transparent"
|
||||
bool has_spending_key = true; // false for view-only (imported via z_importviewingkey)
|
||||
|
||||
// For display
|
||||
std::string label;
|
||||
|
||||
// CONFIRMED balance (minconf>=1) — what z_sendmany can actually spend now. Kept last so positional
|
||||
// brace-init of the leading fields (used in tests) still compiles.
|
||||
double spendableBalance = 0.0;
|
||||
|
||||
// Derived
|
||||
bool isZAddr() const { return !address.empty() && address[0] == 'z'; }
|
||||
bool isShielded() const { return type == "shielded"; }
|
||||
@@ -252,11 +256,17 @@ struct WalletState {
|
||||
// Sync status
|
||||
SyncInfo sync;
|
||||
|
||||
// Balances (named to match UI usage)
|
||||
double privateBalance = 0.0; // shielded balance
|
||||
// Balances (named to match UI usage). These are the DISPLAY totals — minconf=0, so they include the
|
||||
// user's own pending change and don't crater during an unconfirmed send.
|
||||
double privateBalance = 0.0; // shielded balance (display, incl. pending change)
|
||||
double transparentBalance = 0.0;
|
||||
double totalBalance = 0.0;
|
||||
double unconfirmedBalance = 0.0;
|
||||
double unconfirmedBalance = 0.0; // = totalBalance - spendableTotalBalance (the pending portion)
|
||||
// CONFIRMED / spendable totals (minconf>=1) — what can actually be sent right now. z_sendmany runs at
|
||||
// minconf=1, so the Send form / Max / spend validation must size off these, never the display totals.
|
||||
double spendablePrivateBalance = 0.0;
|
||||
double spendableTransparentBalance = 0.0;
|
||||
double spendableTotalBalance = 0.0;
|
||||
|
||||
// Aliases for backward compatibility
|
||||
double& shielded_balance = privateBalance;
|
||||
@@ -325,6 +335,7 @@ struct WalletState {
|
||||
sync = SyncInfo{};
|
||||
privateBalance = transparentBalance = totalBalance = 0.0;
|
||||
unconfirmedBalance = 0.0;
|
||||
spendablePrivateBalance = spendableTransparentBalance = spendableTotalBalance = 0.0;
|
||||
encrypted = false;
|
||||
locked = false;
|
||||
unlocked_until = 0;
|
||||
|
||||
@@ -37,16 +37,27 @@ void applyBalancesFromUnspent(std::vector<AddressInfo>& addresses, const json& u
|
||||
{
|
||||
if (!unspent.is_array()) return;
|
||||
|
||||
std::map<std::string, double> balances;
|
||||
// Partition each note/utxo by its per-entry "confirmations": `total` (minconf=0 — DISPLAY, includes
|
||||
// the user's own pending 0-conf change) vs `spendable` (confirmations>=1 — what z_sendmany, run at
|
||||
// minconf=1, can actually spend). This lets a single z_listunspent(0)/listunspent(0) feed both.
|
||||
std::map<std::string, double> total;
|
||||
std::map<std::string, double> spendable;
|
||||
for (const auto& output : unspent) {
|
||||
auto address = readOptional<std::string>(output, "address");
|
||||
auto amount = readOptional<double>(output, "amount");
|
||||
if (address && amount) balances[*address] += *amount;
|
||||
auto amount = readOptional<double>(output, "amount");
|
||||
if (!address || !amount) continue;
|
||||
total[*address] += *amount;
|
||||
auto conf = readOptional<int>(output, "confirmations");
|
||||
if (conf && *conf >= 1) spendable[*address] += *amount;
|
||||
}
|
||||
|
||||
// The address lists are rebuilt fresh (default 0) each refresh, so hard-set both — an address with no
|
||||
// notes in this set is 0, and spendableBalance is always a subset sum of balance.
|
||||
for (auto& info : addresses) {
|
||||
auto balance = balances.find(info.address);
|
||||
if (balance != balances.end()) info.balance = balance->second;
|
||||
auto t = total.find(info.address);
|
||||
auto s = spendable.find(info.address);
|
||||
info.balance = (t != total.end()) ? t->second : 0.0;
|
||||
info.spendableBalance = (s != spendable.end()) ? s->second : 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +260,7 @@ NetworkRefreshService::ConnectionInitResult NetworkRefreshService::collectConnec
|
||||
}
|
||||
|
||||
NetworkRefreshService::CoreRefreshResult NetworkRefreshService::parseCoreRefreshResult(
|
||||
const json& totalBalance, bool balanceOk, const json& blockInfo, bool blockOk)
|
||||
const json& totalBalance, const json& spendableBalance, bool balanceOk, const json& blockInfo, bool blockOk)
|
||||
{
|
||||
CoreRefreshResult result;
|
||||
result.balanceOk = balanceOk && totalBalance.is_object();
|
||||
@@ -258,6 +269,11 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::parseCoreRefresh
|
||||
result.transparentBalance = readBalanceString(totalBalance, "transparent");
|
||||
result.totalBalance = readBalanceString(totalBalance, "total");
|
||||
}
|
||||
if (spendableBalance.is_object()) { // confirmed totals (minconf=1); left unset on old daemons
|
||||
result.spendableShieldedBalance = readBalanceString(spendableBalance, "private");
|
||||
result.spendableTransparentBalance = readBalanceString(spendableBalance, "transparent");
|
||||
result.spendableTotalBalance = readBalanceString(spendableBalance, "total");
|
||||
}
|
||||
|
||||
result.blockchainOk = blockOk && blockInfo.is_object();
|
||||
if (result.blockchainOk) {
|
||||
@@ -274,17 +290,21 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::parseCoreRefresh
|
||||
NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefreshResult(RefreshRpcGateway& rpc, bool includeBalance)
|
||||
{
|
||||
json totalBalance;
|
||||
json spendableBalance;
|
||||
json blockInfo;
|
||||
bool balanceOk = false;
|
||||
bool blockOk = false;
|
||||
|
||||
if (includeBalance) {
|
||||
try {
|
||||
totalBalance = rpc.call("z_gettotalbalance", json::array());
|
||||
try { // DISPLAY total: minconf=0 — includes the user's own pending change so it doesn't crater
|
||||
totalBalance = rpc.call("z_gettotalbalance", json::array({0}));
|
||||
balanceOk = true;
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("Balance error: %s\n", e.what());
|
||||
}
|
||||
try { // SPENDABLE total: minconf=1 (confirmed). If absent, spendable degrades to display in apply.
|
||||
spendableBalance = rpc.call("z_gettotalbalance", json::array({1}));
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -294,7 +314,7 @@ NetworkRefreshService::CoreRefreshResult NetworkRefreshService::collectCoreRefre
|
||||
DEBUG_LOGF("BlockchainInfo error: %s\n", e.what());
|
||||
}
|
||||
|
||||
return parseCoreRefreshResult(totalBalance, balanceOk, blockInfo, blockOk);
|
||||
return parseCoreRefreshResult(totalBalance, spendableBalance, balanceOk, blockInfo, blockOk);
|
||||
}
|
||||
|
||||
NetworkRefreshService::MiningRefreshResult NetworkRefreshService::parseMiningRefreshResult(
|
||||
@@ -611,15 +631,19 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
}
|
||||
|
||||
try {
|
||||
json unspent = rpc.call("z_listunspent", json::array());
|
||||
json unspent = rpc.call("z_listunspent", json::array({0, 9999999, false})); // minconf=0 → include 0-conf change
|
||||
applyShieldedBalancesFromUnspent(result.shieldedAddresses, unspent);
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("z_listunspent unavailable (%s), falling back to z_getbalance\n", e.what());
|
||||
for (auto& info : result.shieldedAddresses) {
|
||||
try {
|
||||
json balance = rpc.call("z_getbalance", json::array({info.address}));
|
||||
if (!balance.is_null()) info.balance = balance.get<double>();
|
||||
try { // display total (minconf=0, includes pending change)
|
||||
json total = rpc.call("z_getbalance", json::array({info.address, 0}));
|
||||
if (!total.is_null()) info.balance = total.get<double>();
|
||||
} catch (...) {}
|
||||
try { // spendable (minconf=1); degrade to the display value on old daemons
|
||||
json conf = rpc.call("z_getbalance", json::array({info.address, 1}));
|
||||
info.spendableBalance = (!conf.is_null()) ? conf.get<double>() : info.balance;
|
||||
} catch (...) { info.spendableBalance = info.balance; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,7 +655,7 @@ NetworkRefreshService::AddressRefreshResult NetworkRefreshService::collectAddres
|
||||
}
|
||||
|
||||
try {
|
||||
json unspent = rpc.call("listunspent", json::array());
|
||||
json unspent = rpc.call("listunspent", json::array({0})); // minconf=0 → include 0-conf change
|
||||
applyTransparentBalancesFromUnspent(result.transparentAddresses, unspent);
|
||||
} catch (const std::exception& e) {
|
||||
DEBUG_LOGF("listunspent error: %s\n", e.what());
|
||||
@@ -1197,6 +1221,12 @@ void NetworkRefreshService::applyCoreRefreshResult(WalletState& state,
|
||||
if (result.shieldedBalance) state.shielded_balance = *result.shieldedBalance;
|
||||
if (result.transparentBalance) state.transparent_balance = *result.transparentBalance;
|
||||
if (result.totalBalance) state.total_balance = *result.totalBalance;
|
||||
// Confirmed/spendable totals; if the minconf=1 call was unavailable (old daemon) degrade to the
|
||||
// display value so nothing is *over*-reported as spendable (z_sendmany stays the final gate).
|
||||
state.spendablePrivateBalance = result.spendableShieldedBalance.value_or(state.privateBalance);
|
||||
state.spendableTransparentBalance = result.spendableTransparentBalance.value_or(state.transparentBalance);
|
||||
state.spendableTotalBalance = result.spendableTotalBalance.value_or(state.totalBalance);
|
||||
state.unconfirmedBalance = std::max(0.0, state.totalBalance - state.spendableTotalBalance);
|
||||
state.last_balance_update = updatedAt;
|
||||
}
|
||||
|
||||
|
||||
@@ -98,9 +98,12 @@ public:
|
||||
|
||||
struct CoreRefreshResult {
|
||||
bool balanceOk = false;
|
||||
std::optional<double> shieldedBalance;
|
||||
std::optional<double> shieldedBalance; // display (minconf=0, incl. pending change)
|
||||
std::optional<double> transparentBalance;
|
||||
std::optional<double> totalBalance;
|
||||
std::optional<double> spendableShieldedBalance; // confirmed (minconf=1)
|
||||
std::optional<double> spendableTransparentBalance;
|
||||
std::optional<double> spendableTotalBalance;
|
||||
bool blockchainOk = false;
|
||||
std::optional<int> blocks;
|
||||
std::optional<int> headers;
|
||||
@@ -227,6 +230,7 @@ public:
|
||||
RefreshRpcGateway& rpc,
|
||||
const std::optional<ConnectionInfoResult>& prefetchedInfo = std::nullopt);
|
||||
static CoreRefreshResult parseCoreRefreshResult(const nlohmann::json& totalBalance,
|
||||
const nlohmann::json& spendableBalance,
|
||||
bool balanceOk,
|
||||
const nlohmann::json& blockInfo,
|
||||
bool blockOk);
|
||||
|
||||
@@ -324,12 +324,12 @@ void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
s_dragIdx < (int)rows.size()) {
|
||||
const auto& srcRow = rows[s_dragIdx];
|
||||
const auto& dstRow = rows[s_dropTargetIdx];
|
||||
if (srcRow.info->balance > 1e-9) {
|
||||
if (srcRow.info->spendableBalance > 1e-9) { // only offer a transfer of CONFIRMED funds
|
||||
AddressTransferDialog::TransferInfo ti;
|
||||
ti.fromAddr = srcRow.info->address;
|
||||
ti.toAddr = dstRow.info->address;
|
||||
ti.fromBalance = srcRow.info->balance;
|
||||
ti.toBalance = dstRow.info->balance;
|
||||
ti.fromBalance = srcRow.info->spendableBalance; // spend cap — z_sendmany runs at minconf=1
|
||||
ti.toBalance = dstRow.info->balance; // destination display only
|
||||
ti.fromIsZ = srcRow.isZ;
|
||||
ti.toIsZ = dstRow.isZ;
|
||||
AddressTransferDialog::show(app, ti);
|
||||
|
||||
@@ -137,13 +137,15 @@ static double GetAvailableBalance(App* app) {
|
||||
// not a stored list index. The index desyncs from s_from_address after an address-list
|
||||
// refresh, and is left at -1 when the source is chosen from another tab ("Send from this
|
||||
// address") — which previously made the sufficiency check see 0 and block a valid send.
|
||||
// CONFIRMED balance only — this is the spend ceiling (Max button, slider, validation, pre-broadcast
|
||||
// re-check). z_sendmany runs at minconf=1, so offering 0-conf change here would just make the send fail.
|
||||
if (s_from_address[0] != '\0') {
|
||||
for (const auto& a : state.addresses) {
|
||||
if (a.address == s_from_address) return a.balance;
|
||||
if (a.address == s_from_address) return a.spendableBalance;
|
||||
}
|
||||
}
|
||||
if (s_selected_from_idx >= 0 && s_selected_from_idx < static_cast<int>(state.addresses.size())) {
|
||||
return state.addresses[s_selected_from_idx].balance;
|
||||
return state.addresses[s_selected_from_idx].spendableBalance;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
@@ -252,7 +254,7 @@ static void RenderSourceDropdown(App* app, float width) {
|
||||
std::string trunc = util::truncateMiddle(addr.address,
|
||||
static_cast<int>(std::max(S.drawElement("tabs.send", "addr-preview-trunc-min").size, width / S.drawElement("tabs.send", "addr-preview-trunc-divisor").size)));
|
||||
snprintf(buf, sizeof(buf), "%s %s — %.8f %s",
|
||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
|
||||
tag, trunc.c_str(), addr.spendableBalance, DRAGONX_TICKER);
|
||||
s_source_preview = buf;
|
||||
} else {
|
||||
s_source_preview = TR("send_select_source");
|
||||
@@ -282,7 +284,7 @@ static void RenderSourceDropdown(App* app, float width) {
|
||||
|
||||
std::string trunc = util::truncateMiddle(addr.address, (int)addrTruncLen);
|
||||
snprintf(buf, sizeof(buf), "%s %s — %.8f %s",
|
||||
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
|
||||
tag, trunc.c_str(), addr.spendableBalance, DRAGONX_TICKER);
|
||||
|
||||
ImGui::PushID(static_cast<int>(i));
|
||||
if (ImGui::Selectable(buf, isCurrent)) {
|
||||
@@ -292,7 +294,7 @@ static void RenderSourceDropdown(App* app, float width) {
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
material::Tooltip("%s\nBalance: %.8f %s",
|
||||
addr.address.c_str(), addr.balance, DRAGONX_TICKER);
|
||||
addr.address.c_str(), addr.spendableBalance, DRAGONX_TICKER);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
@@ -139,6 +139,12 @@ void applyLiteRefreshModelToWalletState(const LiteWalletAppRefreshModel& model,
|
||||
state.transparentBalance = static_cast<double>(model.balance.transparentZatoshis) / kZatoshisPerCoin;
|
||||
state.totalBalance = static_cast<double>(model.balance.totalZatoshis) / kZatoshisPerCoin;
|
||||
state.unconfirmedBalance = static_cast<double>(model.balance.unconfirmedZatoshis) / kZatoshisPerCoin;
|
||||
// Lite already tracks confirmed/unconfirmed itself and its per-address balances are confirmed
|
||||
// (spendable outputs only). Mirror the aggregate spendable fields so full-node-shaped spend
|
||||
// validation isn't zeroed on lite (the actual lite spend gate is per-address, set below).
|
||||
state.spendablePrivateBalance = state.privateBalance;
|
||||
state.spendableTransparentBalance = state.transparentBalance;
|
||||
state.spendableTotalBalance = state.totalBalance;
|
||||
}
|
||||
|
||||
if (model.hasAddresses) {
|
||||
@@ -178,6 +184,7 @@ void applyLiteRefreshModelToWalletState(const LiteWalletAppRefreshModel& model,
|
||||
} else {
|
||||
info.balance = 0.0; // notes succeeded and address has no spendable outputs
|
||||
}
|
||||
info.spendableBalance = info.balance; // lite per-address balance is already confirmed/spendable
|
||||
info.type = (addr.kind == LiteWalletAppAddressKind::Shielded) ? "shielded" : "transparent";
|
||||
info.has_spending_key = addr.spendabilityKnown ? addr.spendable : true;
|
||||
if (addr.kind == LiteWalletAppAddressKind::Shielded) {
|
||||
|
||||
@@ -934,6 +934,7 @@ void testSpendableFiltering()
|
||||
addresses.push_back({"zs-low", 2.0, "shielded", true});
|
||||
addresses.push_back({"R-zero", 0.0, "transparent", true});
|
||||
addresses.push_back({"R-high", 5.0, "transparent", true});
|
||||
for (auto& a : addresses) a.spendableBalance = a.balance; // selection now ranks by CONFIRMED balance
|
||||
|
||||
EXPECT_EQ(dragonx::bestSpendableAddressIndex(addresses), 3);
|
||||
|
||||
@@ -1424,10 +1425,10 @@ void testNetworkRefreshRpcCollectors()
|
||||
});
|
||||
auto core = Refresh::collectCoreRefreshResult(coreRpc);
|
||||
EXPECT_TRUE(coreRpc.methodNames() == std::vector<std::string>({
|
||||
"z_gettotalbalance", "getblockchaininfo"
|
||||
"z_gettotalbalance", "z_gettotalbalance", "getblockchaininfo" // display (minconf=0) + spendable (minconf=1)
|
||||
}));
|
||||
EXPECT_EQ(coreRpc.calls[0].params, json::array());
|
||||
EXPECT_EQ(coreRpc.calls[1].params, json::array());
|
||||
EXPECT_EQ(coreRpc.calls[0].params, json::array({0}));
|
||||
EXPECT_EQ(coreRpc.calls[1].params, json::array({1}));
|
||||
EXPECT_TRUE(core.balanceOk);
|
||||
EXPECT_TRUE(core.blockchainOk);
|
||||
EXPECT_NEAR(*core.totalBalance, 4.25, 0.00000001);
|
||||
@@ -1440,7 +1441,7 @@ void testNetworkRefreshRpcCollectors()
|
||||
coreFallbackRpc.addResponse("getblockchaininfo", json{{"blocks", 8}, {"headers", 9}});
|
||||
auto partialCore = Refresh::collectCoreRefreshResult(coreFallbackRpc);
|
||||
EXPECT_TRUE(coreFallbackRpc.methodNames() == std::vector<std::string>({
|
||||
"z_gettotalbalance", "getblockchaininfo"
|
||||
"z_gettotalbalance", "z_gettotalbalance", "getblockchaininfo"
|
||||
}));
|
||||
EXPECT_FALSE(partialCore.balanceOk);
|
||||
EXPECT_TRUE(partialCore.blockchainOk);
|
||||
@@ -1566,7 +1567,7 @@ void testNetworkRefreshRpcCollectors()
|
||||
auto fallbackAddresses = Refresh::collectAddressRefreshResult(fallbackRpc);
|
||||
EXPECT_TRUE(fallbackRpc.methodNames() == std::vector<std::string>({
|
||||
"z_listaddresses", "z_validateaddress", "z_listunspent",
|
||||
"z_getbalance", "getaddressesbyaccount", "listunspent"
|
||||
"z_getbalance", "z_getbalance", "getaddressesbyaccount", "listunspent" // display (minconf=0) + spendable (minconf=1)
|
||||
}));
|
||||
EXPECT_TRUE(fallbackAddresses.shieldedAddresses[0].has_spending_key);
|
||||
EXPECT_NEAR(fallbackAddresses.shieldedAddresses[0].balance, 4.75, 0.00000001);
|
||||
@@ -1963,7 +1964,8 @@ void testNetworkRefreshResultModels()
|
||||
dragonx::WalletState state;
|
||||
|
||||
auto core = Refresh::parseCoreRefreshResult(
|
||||
json{{"private", "1.25000000"}, {"transparent", "0.50000000"}, {"total", "1.75000000"}},
|
||||
json{{"private", "1.25000000"}, {"transparent", "0.50000000"}, {"total", "1.75000000"}}, // display (minconf=0)
|
||||
json{{"private", "1.00000000"}, {"transparent", "0.50000000"}, {"total", "1.50000000"}}, // spendable (minconf=1)
|
||||
true,
|
||||
json{{"blocks", 100}, {"headers", 105}, {"bestblockhash", "apply-best-100"}, {"verificationprogress", 0.75},
|
||||
{"longestchain", 110}, {"notarized", 90}},
|
||||
@@ -1972,6 +1974,10 @@ void testNetworkRefreshResultModels()
|
||||
EXPECT_NEAR(state.shielded_balance, 1.25, 0.00000001);
|
||||
EXPECT_NEAR(state.transparent_balance, 0.5, 0.00000001);
|
||||
EXPECT_NEAR(state.total_balance, 1.75, 0.00000001);
|
||||
// Confirmed/spendable stays at minconf=1; unconfirmed = display total - spendable total (the pending change).
|
||||
EXPECT_NEAR(state.spendablePrivateBalance, 1.0, 0.00000001);
|
||||
EXPECT_NEAR(state.spendableTotalBalance, 1.5, 0.00000001);
|
||||
EXPECT_NEAR(state.unconfirmedBalance, 0.25, 0.00000001);
|
||||
EXPECT_EQ(state.sync.blocks, 100);
|
||||
EXPECT_EQ(state.sync.headers, 105);
|
||||
EXPECT_EQ(state.sync.best_blockhash, std::string("apply-best-100"));
|
||||
|
||||
Reference in New Issue
Block a user