diff --git a/src/app_network.cpp b/src/app_network.cpp index 91f3083..e85d17b 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -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& 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 } diff --git a/src/app_sweep.cpp b/src/app_sweep.cpp index cb6407a..000865e 100644 --- a/src/app_sweep.cpp +++ b/src/app_sweep.cpp @@ -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"), diff --git a/src/data/wallet_state.cpp b/src/data/wallet_state.cpp index 880ec6b..2767c01 100644 --- a/src/data/wallet_state.cpp +++ b/src/data/wallet_state.cpp @@ -19,12 +19,13 @@ std::vector sortedSpendableAddressIndices(const std::vector 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& 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(i); } } diff --git a/src/data/wallet_state.h b/src/data/wallet_state.h index 66a1ba3..988110d 100644 --- a/src/data/wallet_state.h +++ b/src/data/wallet_state.h @@ -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,12 +256,18 @@ 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; double& transparent_balance = transparentBalance; @@ -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; diff --git a/src/services/network_refresh_service.cpp b/src/services/network_refresh_service.cpp index 03c1887..2abcd31 100644 --- a/src/services/network_refresh_service.cpp +++ b/src/services/network_refresh_service.cpp @@ -37,16 +37,27 @@ void applyBalancesFromUnspent(std::vector& addresses, const json& u { if (!unspent.is_array()) return; - std::map 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 total; + std::map spendable; for (const auto& output : unspent) { auto address = readOptional(output, "address"); - auto amount = readOptional(output, "amount"); - if (address && amount) balances[*address] += *amount; + auto amount = readOptional(output, "amount"); + if (!address || !amount) continue; + total[*address] += *amount; + auto conf = readOptional(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(); + 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(); } 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() : 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; } diff --git a/src/services/network_refresh_service.h b/src/services/network_refresh_service.h index efdf024..7fd1b88 100644 --- a/src/services/network_refresh_service.h +++ b/src/services/network_refresh_service.h @@ -98,9 +98,12 @@ public: struct CoreRefreshResult { bool balanceOk = false; - std::optional shieldedBalance; + std::optional shieldedBalance; // display (minconf=0, incl. pending change) std::optional transparentBalance; std::optional totalBalance; + std::optional spendableShieldedBalance; // confirmed (minconf=1) + std::optional spendableTransparentBalance; + std::optional spendableTotalBalance; bool blockchainOk = false; std::optional blocks; std::optional headers; @@ -227,6 +230,7 @@ public: RefreshRpcGateway& rpc, const std::optional& prefetchedInfo = std::nullopt); static CoreRefreshResult parseCoreRefreshResult(const nlohmann::json& totalBalance, + const nlohmann::json& spendableBalance, bool balanceOk, const nlohmann::json& blockInfo, bool blockOk); diff --git a/src/ui/windows/balance_components.cpp b/src/ui/windows/balance_components.cpp index 8ee7d78..2a836bf 100644 --- a/src/ui/windows/balance_components.cpp +++ b/src/ui/windows/balance_components.cpp @@ -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); diff --git a/src/ui/windows/send_tab.cpp b/src/ui/windows/send_tab.cpp index 1bee4fd..0b3c0f5 100644 --- a/src/ui/windows/send_tab.cpp +++ b/src/ui/windows/send_tab.cpp @@ -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(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(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(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(); } diff --git a/src/wallet/lite_wallet_controller.cpp b/src/wallet/lite_wallet_controller.cpp index d1bc0e4..1b0d891 100644 --- a/src/wallet/lite_wallet_controller.cpp +++ b/src/wallet/lite_wallet_controller.cpp @@ -139,6 +139,12 @@ void applyLiteRefreshModelToWalletState(const LiteWalletAppRefreshModel& model, state.transparentBalance = static_cast(model.balance.transparentZatoshis) / kZatoshisPerCoin; state.totalBalance = static_cast(model.balance.totalZatoshis) / kZatoshisPerCoin; state.unconfirmedBalance = static_cast(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) { diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index c420a7d..1d33034 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -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({ - "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({ - "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({ "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"));