From 530bf24abf9149b6266cd15266a07d8dbe17a162 Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 2 Jul 2026 03:27:44 -0500 Subject: [PATCH] fix(market): populate combined address list so portfolio sees addresses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Market-tab portfolio (editor checklist + SumPortfolioBalance) reads the combined WalletState::addresses view, but the full-node refresh path only ever updated the authoritative z_addresses/t_addresses lists — rebuildAddressList() was never called, so `addresses` stayed empty. Result: the manage-portfolio modal showed no addresses to pick, and addresses added via the overview right-click menu contributed no value to their entry (SumPortfolioBalance found nothing in the empty combined list). Rebuild the combined view in NetworkRefreshService::applyAddressRefreshResult (the sole bulk updater of the address lists), and push newly created addresses into the combined view immediately in the full-node create paths for zero-latency parity with the overview (matching the lite branch). This also repairs two other silently-broken full-node consumers of state.addresses: the auto-shield target-address finder and the pool-mining transparent fallback. Add a regression test (testWalletStateAddressListRebuild) covering the empty-before / union-after rebuild and pending-send delta reflection. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app_network.cpp | 2 ++ src/services/network_refresh_service.cpp | 5 +++ tests/test_phase4.cpp | 41 ++++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/src/app_network.cpp b/src/app_network.cpp index 204f3b0..9f5c7e3 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -1980,6 +1980,7 @@ void App::createNewZAddress(std::function callback) info.type = "shielded"; info.balance = 0.0; state_.z_addresses.push_back(info); + state_.addresses.push_back(info); // keep combined view in sync immediately address_list_dirty_ = true; // Also trigger full refresh to get proper balances addresses_dirty_ = true; @@ -2027,6 +2028,7 @@ void App::createNewTAddress(std::function callback) info.type = "transparent"; info.balance = 0.0; state_.t_addresses.push_back(info); + state_.addresses.push_back(info); // keep combined view in sync immediately address_list_dirty_ = true; // Also trigger full refresh to get proper balances addresses_dirty_ = true; diff --git a/src/services/network_refresh_service.cpp b/src/services/network_refresh_service.cpp index e8b462f..fe94507 100644 --- a/src/services/network_refresh_service.cpp +++ b/src/services/network_refresh_service.cpp @@ -1255,6 +1255,11 @@ void NetworkRefreshService::applyAddressRefreshResult(WalletState& state, { state.z_addresses = std::move(result.shieldedAddresses); state.t_addresses = std::move(result.transparentAddresses); + // Keep the combined `addresses` view in sync with the authoritative z/t lists. + // Consumers that read state.addresses (Market-tab portfolio, auto-shield target + // selection, pool-mining transparent fallback) depend on this being rebuilt — the + // full-node refresh path is the only place that bulk-updates the address lists. + state.rebuildAddressList(); } void NetworkRefreshService::applyTransactionRefreshResult(WalletState& state, diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 4bc8ba3..ef770ce 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -2622,6 +2622,46 @@ void testPortfolioHelpers() EXPECT_EQ(group[0], std::string("R-t2")); } +// Regression: the Market-tab portfolio (and other consumers) read the combined +// WalletState::addresses view, which the full-node refresh path builds from the +// authoritative z/t lists via rebuildAddressList(). Before the fix that rebuild +// was never called, so `addresses` stayed empty and every portfolio group summed +// to 0 and showed no addresses to pick. Guard that the rebuilt view carries the +// z/t union with balances, so SumPortfolioBalance resolves right-click-added +// addresses. +void testWalletStateAddressListRebuild() +{ + using dragonx::AddressInfo; + using dragonx::data::SumPortfolioBalance; + + dragonx::WalletState state; + auto mk = [](const char* addr, double bal, const char* type) { + AddressInfo a; a.address = addr; a.balance = bal; a.type = type; return a; + }; + state.z_addresses.push_back(mk("zs-s1", 10.0, "shielded")); + state.z_addresses.push_back(mk("zs-s2", 0.25, "shielded")); + state.t_addresses.push_back(mk("R-t1", 1.5, "transparent")); + + // Before the rebuild the combined view is empty -> the reported bug: a portfolio + // group of real addresses sums to 0 because none are found in state.addresses. + EXPECT_EQ(state.addresses.size(), static_cast(0)); + EXPECT_NEAR(SumPortfolioBalance({"zs-s1", "R-t1"}, state.addresses), 0.0, 1e-9); + + // After the rebuild the combined view is the z/t union with balances. + state.rebuildAddressList(); + EXPECT_EQ(state.addresses.size(), static_cast(3)); + // z entries come first, then t entries (rebuild order). + EXPECT_EQ(state.addresses.front().address, std::string("zs-s1")); + EXPECT_EQ(state.addresses.back().address, std::string("R-t1")); + // A group spanning both pools now resolves its balances. + EXPECT_NEAR(SumPortfolioBalance({"zs-s1", "R-t1"}, state.addresses), 11.5, 1e-9); + + // In-place z/t balance mutation (pending-send delta) is reflected on the next rebuild. + state.z_addresses[0].balance = 4.0; + state.rebuildAddressList(); + EXPECT_NEAR(SumPortfolioBalance({"zs-s1", "R-t1"}, state.addresses), 5.5, 1e-9); +} + // ComputeConsoleFoldSpans — brace/bracket matching over pretty-printed JSON lines. void testConsoleFoldSpans() { @@ -5353,6 +5393,7 @@ int main() testConsoleTextLayout(); testConsoleModel(); testPortfolioHelpers(); + testWalletStateAddressListRebuild(); testConsoleFoldSpans(); testConsoleScrollController(); testConsoleSelectionController();