Files
ObsidianDragon/src/data/exchange_info.cpp
DanS 326192e75e feat(market): per-exchange 24h volume in the hero
The hero's "24H Vol" showed CoinGecko's cross-exchange aggregate even after the
chart/price went per-venue. Capture the per-exchange converted_volume.usd from
the tickers (previously discarded, alongside converted_last) and show the
SELECTED exchange's own 24h volume; fall back to the aggregate when unknown.
Market cap stays aggregate (it's coin-wide, not per-venue).

This is a big real difference — e.g. Ourbit ~$14.3K vs NonKYC ~$253 for DRGX/USDT
— so the header now reflects the venue you're actually looking at.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 10:58:50 -05:00

101 lines
3.8 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "exchange_info.h"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <unordered_map>
namespace dragonx {
namespace data {
const std::vector<ExchangeInfo>& getExchangeRegistry()
{
// Offline seed / fallback. The live list from CoinGecko (parseCoinGeckoTickers)
// supersedes this when available; keep the known venues here so the tab is never
// empty without network.
static const std::vector<ExchangeInfo> registry = {
{
"Nonkyc.io",
"https://nonkyc.io",
{
{"DRGX", "USDT", "DRGX/USDT", "https://nonkyc.io/market/DRGX_USDT", "nonkyc_io"},
}
},
{
"OurBit",
"https://www.ourbit.com",
{
{"DRGX", "USDT", "DRGX/USDT", "https://www.ourbit.com/exchange/DRGX_USDT", "ourbit"},
}
},
};
return registry;
}
namespace {
// Extract the scheme://host origin from a URL (for ExchangeInfo.baseUrl).
std::string originOf(const std::string& url)
{
const auto scheme = url.find("://");
if (scheme == std::string::npos) return url;
const auto host = url.find('/', scheme + 3);
return (host == std::string::npos) ? url : url.substr(0, host);
}
} // namespace
std::vector<ExchangeInfo> parseCoinGeckoTickers(const std::string& body)
{
std::vector<ExchangeInfo> exchanges;
try {
const nlohmann::json j = nlohmann::json::parse(body);
if (!j.contains("tickers") || !j["tickers"].is_array()) return {};
std::unordered_map<std::string, size_t> byName; // exchange name -> index
for (const auto& t : j["tickers"]) {
if (!t.is_object()) continue;
const std::string base = t.value("base", std::string{});
const std::string target = t.value("target", std::string{});
std::string market, identifier;
if (t.contains("market") && t["market"].is_object()) {
market = t["market"].value("name", std::string{});
identifier = t["market"].value("identifier", std::string{}); // keys the per-exchange candle adapter
}
const std::string tradeUrl = t.value("trade_url", std::string{});
if (base.empty() || target.empty() || market.empty()) continue;
double lastUsd = 0.0; // this venue's current USD price + 24h volume — differ per exchange
if (t.contains("converted_last") && t["converted_last"].is_object())
lastUsd = t["converted_last"].value("usd", 0.0);
double volumeUsd = 0.0;
if (t.contains("converted_volume") && t["converted_volume"].is_object())
volumeUsd = t["converted_volume"].value("usd", 0.0);
auto it = byName.find(market);
if (it == byName.end()) {
byName[market] = exchanges.size();
exchanges.push_back(ExchangeInfo{market, originOf(tradeUrl), {}});
it = byName.find(market);
}
ExchangePair pair{base, target, base + "/" + target, tradeUrl, identifier, lastUsd, volumeUsd};
// Skip duplicate pairs on the same exchange.
bool dup = false;
for (const auto& p : exchanges[it->second].pairs)
if (p.displayName == pair.displayName) { dup = true; break; }
if (!dup) exchanges[it->second].pairs.push_back(std::move(pair));
}
} catch (...) {
return {};
}
// Drop exchanges that ended up with no pairs (defensive).
exchanges.erase(std::remove_if(exchanges.begin(), exchanges.end(),
[](const ExchangeInfo& e) { return e.pairs.empty(); }),
exchanges.end());
return exchanges;
}
} // namespace data
} // namespace dragonx