From 3f82af99c46347ac51caa91b2a15e7bd322d8acf Mon Sep 17 00:00:00 2001 From: DanS Date: Tue, 13 Oct 2026 16:00:10 -0500 Subject: [PATCH] refactor(market): resolve exchange/pair selection through one helper The chart fetch and the Market tab both resolved the saved exchange and pair names into the registry, with their own near-identical scans and slightly different fallbacks. Keeping two copies risked the chart and the chips disagreeing about which pair was selected. - A single resolver maps the saved names to a registry index, matching the exchange name (any exchange when none is saved) then the pair display name, and falling back to the registry's first pair, the UI default. The chart fetch and the Market tab both call it. - The Market tab now derives its selection indices from the saved names against the current registry every frame, so the indices stay correct when the live exchange list replaces the fallback; a steady-state fast path skips the scan once the indices already name the saved selection. Tests: the resolver returns the right index for a matched exchange/pair, an empty-exchange match, and the first-pair fallback on misses. Co-Authored-By: Claude Opus 4.8 --- src/app_network.cpp | 11 +++++---- src/data/exchange_info.h | 20 +++++++++++++++++ src/ui/windows/market_tab.cpp | 42 +++++++++++++++++++---------------- tests/test_phase4.cpp | 26 ++++++++++++++++++++++ 4 files changed, 74 insertions(+), 25 deletions(-) diff --git a/src/app_network.cpp b/src/app_network.cpp index f3665d6..9f23ca1 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -3272,14 +3272,13 @@ void App::refreshExchangeChart() ? data::getExchangeRegistry() : state_.market.exchanges; const std::string selEx = settings_->getSelectedExchange(); const std::string selPair = settings_->getSelectedPair(); + // Same resolver as the Market tab chips (not found => registry's first pair, the UI default). const data::ExchangePair* pair = nullptr; - for (const auto& ex : reg) { - if (!selEx.empty() && ex.name != selEx) continue; - for (const auto& p : ex.pairs) - if (p.displayName == selPair) { pair = &p; break; } - if (pair) break; + if (!reg.empty()) { + const auto sel = data::resolveExchangeSelection(reg, selEx, selPair); + if (sel.exchangeIdx < (int)reg.size() && sel.pairIdx < (int)reg[sel.exchangeIdx].pairs.size()) + pair = ®[sel.exchangeIdx].pairs[sel.pairIdx]; } - if (!pair && !reg.empty() && !reg[0].pairs.empty()) pair = ®[0].pairs[0]; // default like the UI if (!pair) { state_.market.exchange_chart_active = false; exchange_chart_key_.clear(); return; } const std::string key = pair->identifier + ":" + pair->base + "/" + pair->quote; diff --git a/src/data/exchange_info.h b/src/data/exchange_info.h index 3973088..36a2f20 100644 --- a/src/data/exchange_info.h +++ b/src/data/exchange_info.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include @@ -38,6 +39,25 @@ struct ExchangeInfo { */ const std::vector& getExchangeRegistry(); +/** + * @brief Index of the selected exchange/pair in `reg`, resolved by saved NAME. + * Matches the exchange name (any exchange when selEx is empty) THEN the pair display name, like the chart + * fetch always has. Not found => (0,0), found=false (the registry's first pair - the app's default). + * Single source of truth for the Market tab chips and App::refreshExchangeChart so they cannot diverge. + */ +struct ExchangeSelection { int exchangeIdx = 0; int pairIdx = 0; bool found = false; }; +inline ExchangeSelection resolveExchangeSelection(const std::vector& reg, + const std::string& selEx, const std::string& selPair) +{ + for (std::size_t ei = 0; ei < reg.size(); ++ei) { + if (!selEx.empty() && reg[ei].name != selEx) continue; + for (std::size_t pi = 0; pi < reg[ei].pairs.size(); ++pi) + if (reg[ei].pairs[pi].displayName == selPair) + return {static_cast(ei), static_cast(pi), true}; + } + return {}; +} + /** * @brief Parse a CoinGecko /coins/{id}/tickers JSON body into the exchange registry. * diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 948bd0e..31f9087 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -56,28 +56,12 @@ static const std::vector& EffectiveRegistry(const MarketInfo return market.exchanges.empty() ? data::getExchangeRegistry() : market.exchanges; } -// Helper: load selected exchange/pair from settings -static void LoadMarketState(config::Settings* settings, const std::vector& registry) +// One-shot restore of the persisted chart range + style (idempotent: both are re-persisted on click). +static void LoadMarketState(config::Settings* settings) { if (s_mkt.stateLoaded || !settings) return; s_mkt.stateLoaded = true; - std::string savedExchange = settings->getSelectedExchange(); - std::string savedPair = settings->getSelectedPair(); - - for (int ei = 0; ei < (int)registry.size(); ei++) { - if (registry[ei].name == savedExchange) { - s_mkt.exchangeIdx = ei; - for (int pi = 0; pi < (int)registry[ei].pairs.size(); pi++) { - if (registry[ei].pairs[pi].displayName == savedPair) { - s_mkt.pairIdx = pi; - break; - } - } - break; - } - } - // Restore the chart range + style (validated). int iv = settings->getChartInterval(); if (iv >= 0 && iv <= 4) s_mkt.chartInterval = iv; @@ -85,6 +69,25 @@ static void LoadMarketState(config::Settings* settings, const std::vector& registry) +{ + if (!settings || registry.empty()) return; + const std::string selEx = settings->getSelectedExchange(); + const std::string selPair = settings->getSelectedPair(); + // Steady state: the current indices already name the saved selection -> no scan. + if (s_mkt.exchangeIdx >= 0 && s_mkt.exchangeIdx < (int)registry.size()) { + const auto& ex = registry[s_mkt.exchangeIdx]; + if (ex.name == selEx && s_mkt.pairIdx >= 0 && s_mkt.pairIdx < (int)ex.pairs.size() && + ex.pairs[s_mkt.pairIdx].displayName == selPair) return; + } + const auto sel = data::resolveExchangeSelection(registry, selEx, selPair); + s_mkt.exchangeIdx = sel.exchangeIdx; + s_mkt.pairIdx = sel.pairIdx; +} + // Helper: format compact currency static std::string FormatCompactUSD(double val) { @@ -2242,7 +2245,8 @@ void RenderMarketTab(App* app) const auto& registry = EffectiveRegistry(market); // Load persisted exchange/pair on first frame - LoadMarketState(app->settings(), registry); + LoadMarketState(app->settings()); + ResolveMarketSelection(app->settings(), registry); if (s_mkt.exchangeIdx >= (int)registry.size()) s_mkt.exchangeIdx = 0; const auto& currentExchange = registry[s_mkt.exchangeIdx]; diff --git a/tests/test_phase4.cpp b/tests/test_phase4.cpp index 4e179b3..5868bb6 100644 --- a/tests/test_phase4.cpp +++ b/tests/test_phase4.cpp @@ -12,6 +12,7 @@ #include #include #endif +#include "data/exchange_info.h" #include "util/text_format.h" #include "chat/chat_crypto.h" #include "chat/chat_identity.h" @@ -17958,6 +17959,30 @@ void testLoadLanguageFromLooseFiles() EXPECT_EQ(std::string(i18n.translate("overview")), std::string("Overview")); } +void testResolveExchangeSelection() +{ + using dragonx::data::ExchangeInfo; + using dragonx::data::ExchangePair; + auto mk = [](const char* n, std::initializer_list pairs) { + ExchangeInfo e; e.name = n; + for (auto* d : pairs) { ExchangePair p; p.displayName = d; e.pairs.push_back(p); } + return e; + }; + std::vector reg = { mk("A", {"X/BTC", "X/USDT"}), mk("B", {"X/BTC", "Y/BTC"}) }; + auto r = dragonx::data::resolveExchangeSelection(reg, "B", "Y/BTC"); + EXPECT_TRUE(r.found); EXPECT_EQ(r.exchangeIdx, 1); EXPECT_EQ(r.pairIdx, 1); + r = dragonx::data::resolveExchangeSelection(reg, "B", "X/BTC"); + EXPECT_TRUE(r.found); EXPECT_EQ(r.exchangeIdx, 1); EXPECT_EQ(r.pairIdx, 0); + r = dragonx::data::resolveExchangeSelection(reg, "", "Y/BTC"); // empty exchange => any + EXPECT_TRUE(r.found); EXPECT_EQ(r.exchangeIdx, 1); EXPECT_EQ(r.pairIdx, 1); + r = dragonx::data::resolveExchangeSelection(reg, "A", "Y/BTC"); // pair not on that exchange + EXPECT_TRUE(!r.found); EXPECT_EQ(r.exchangeIdx, 0); EXPECT_EQ(r.pairIdx, 0); + r = dragonx::data::resolveExchangeSelection(reg, "Z", "X/BTC"); // unknown exchange + EXPECT_TRUE(!r.found); EXPECT_EQ(r.exchangeIdx, 0); EXPECT_EQ(r.pairIdx, 0); + r = dragonx::data::resolveExchangeSelection({}, "A", "X/BTC"); + EXPECT_TRUE(!r.found); EXPECT_EQ(r.exchangeIdx, 0); +} + void testCaptureExtent() { using namespace dragonx::ui::effects; @@ -18406,6 +18431,7 @@ int main() testLoadLanguageFromLooseFiles(); testWriteFileIfDifferent(); testCaptureExtent(); + testResolveExchangeSelection(); if (g_failures != 0) { std::cerr << g_failures << " test assertion(s) failed\n";