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";