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