diff --git a/data/static/currencyacronymtranslator.json b/data/static/currencyacronymtranslator.json index 9837a50e..2c7d481f 100644 --- a/data/static/currencyacronymtranslator.json +++ b/data/static/currencyacronymtranslator.json @@ -9,8 +9,7 @@ "XLTC": "LTC", "XXLM": "XLM", "XMLN": "MLN", - "XXDG": "XDG", - "DOGE": "XDG", + "XXDG": "DOGE", "XZEC": "ZEC", "ZEUR": "EUR", "ZCAD": "CAD", diff --git a/src/engine/include/coincenter.hpp b/src/engine/include/coincenter.hpp index 73d9f456..682eaf7d 100644 --- a/src/engine/include/coincenter.hpp +++ b/src/engine/include/coincenter.hpp @@ -33,6 +33,9 @@ class Coincenter { ExchangeHealthCheckStatus healthCheck(ExchangeNameSpan exchangeNames); + /// Retrieve all tradable currencies for given selected public exchanges, or all if empty. + CurrenciesPerExchange getCurrenciesPerExchange(ExchangeNameSpan exchangeNames); + /// Retrieve the markets for given selected public exchanges, or all if empty. MarketsPerExchange getMarketsPerExchange(CurrencyCode cur1, CurrencyCode cur2, ExchangeNameSpan exchangeNames); diff --git a/src/engine/include/coincenteroptions.hpp b/src/engine/include/coincenteroptions.hpp index 76019dbc..3bb608c0 100644 --- a/src/engine/include/coincenteroptions.hpp +++ b/src/engine/include/coincenteroptions.hpp @@ -44,6 +44,7 @@ class CoincenterCmdLineOptions { std::string_view monitoringUsername; std::string_view monitoringPassword; + std::optional currencies; std::string_view markets; std::string_view orderbook; diff --git a/src/engine/include/coincenteroptionsdef.hpp b/src/engine/include/coincenteroptionsdef.hpp index dcc90b80..61c9f183 100644 --- a/src/engine/include/coincenteroptionsdef.hpp +++ b/src/engine/include/coincenteroptionsdef.hpp @@ -173,6 +173,12 @@ struct CoincenterAllowedOptions : private CoincenterCmdLineOptionsDefinitions { "<[exch1,...]>", "Simple health check for all exchanges or specified ones"}, &OptValueType::healthCheck}, + {{{"Public queries", 2100}, + "currencies", + "<[exch1,...]>", + "Print tradable currencies for all exchanges, " + "or only the specified ones."}, + &OptValueType::currencies}, {{{"Public queries", 2100}, "markets", "", diff --git a/src/engine/include/exchangesorchestrator.hpp b/src/engine/include/exchangesorchestrator.hpp index ddab414e..d49601fb 100644 --- a/src/engine/include/exchangesorchestrator.hpp +++ b/src/engine/include/exchangesorchestrator.hpp @@ -44,6 +44,8 @@ class ExchangesOrchestrator { ConversionPathPerExchange getConversionPaths(Market mk, ExchangeNameSpan exchangeNames); + CurrenciesPerExchange getCurrenciesPerExchange(ExchangeNameSpan exchangeNames); + MarketsPerExchange getMarketsPerExchange(CurrencyCode cur1, CurrencyCode cur2, ExchangeNameSpan exchangeNames); UniquePublicSelectedExchanges getExchangesTradingCurrency(CurrencyCode currencyCode, ExchangeNameSpan exchangeNames, diff --git a/src/engine/include/queryresultprinter.hpp b/src/engine/include/queryresultprinter.hpp index c0a969f8..4e64f5e8 100644 --- a/src/engine/include/queryresultprinter.hpp +++ b/src/engine/include/queryresultprinter.hpp @@ -33,6 +33,8 @@ class QueryResultPrinter { void printHealthCheck(const ExchangeHealthCheckStatus &healthCheckPerExchange) const; + void printCurrencies(const CurrenciesPerExchange ¤ciesPerExchange) const; + void printMarkets(CurrencyCode cur1, CurrencyCode cur2, const MarketsPerExchange &marketsPerExchange) const; void printMarketOrderBooks(Market mk, CurrencyCode equiCurrencyCode, std::optional depth, diff --git a/src/engine/include/queryresulttypes.hpp b/src/engine/include/queryresulttypes.hpp index a25c855e..5cd9263a 100644 --- a/src/engine/include/queryresulttypes.hpp +++ b/src/engine/include/queryresulttypes.hpp @@ -10,6 +10,7 @@ #include "cct_const.hpp" #include "cct_fixedcapacityvector.hpp" #include "cct_smallvector.hpp" +#include "currencyexchangeflatset.hpp" #include "exchangeprivateapitypes.hpp" #include "exchangepublicapitypes.hpp" #include "marketorderbook.hpp" @@ -43,6 +44,8 @@ using ExchangeHealthCheckStatus = FixedCapacityVector, kNbSup using ExchangeTickerMaps = FixedCapacityVector, kNbSupportedExchanges>; +using CurrenciesPerExchange = FixedCapacityVector, kNbSupportedExchanges>; + using BalancePerExchange = SmallVector, kTypicalNbPrivateAccounts>; using WalletPerExchange = SmallVector, kTypicalNbPrivateAccounts>; diff --git a/src/engine/src/coincenter.cpp b/src/engine/src/coincenter.cpp index fd0d7e6e..b12de533 100644 --- a/src/engine/src/coincenter.cpp +++ b/src/engine/src/coincenter.cpp @@ -82,6 +82,11 @@ TransferableCommandResultVector Coincenter::processCommand( _queryResultPrinter.printHealthCheck(healthCheckStatus); break; } + case CoincenterCommandType::kCurrencies: { + const auto currenciesPerExchange = getCurrenciesPerExchange(cmd.exchangeNames()); + _queryResultPrinter.printCurrencies(currenciesPerExchange); + break; + } case CoincenterCommandType::kMarkets: { const auto marketsPerExchange = getMarketsPerExchange(cmd.cur1(), cmd.cur2(), cmd.exchangeNames()); _queryResultPrinter.printMarkets(cmd.cur1(), cmd.cur2(), marketsPerExchange); @@ -292,6 +297,10 @@ ConversionPathPerExchange Coincenter::getConversionPaths(Market mk, ExchangeName return _exchangesOrchestrator.getConversionPaths(mk, exchangeNames); } +CurrenciesPerExchange Coincenter::getCurrenciesPerExchange(ExchangeNameSpan exchangeNames) { + return _exchangesOrchestrator.getCurrenciesPerExchange(exchangeNames); +} + MarketsPerExchange Coincenter::getMarketsPerExchange(CurrencyCode cur1, CurrencyCode cur2, ExchangeNameSpan exchangeNames) { return _exchangesOrchestrator.getMarketsPerExchange(cur1, cur2, exchangeNames); diff --git a/src/engine/src/coincentercommand.cpp b/src/engine/src/coincentercommand.cpp index afed84be..5732e6a9 100644 --- a/src/engine/src/coincentercommand.cpp +++ b/src/engine/src/coincentercommand.cpp @@ -19,6 +19,8 @@ bool CoincenterCommand::isPublic() const { switch (_type) { case CoincenterCommandType::kHealthCheck: // NOLINT(bugprone-branch-clone) [[fallthrough]]; + case CoincenterCommandType::kCurrencies: + [[fallthrough]]; case CoincenterCommandType::kMarkets: [[fallthrough]]; case CoincenterCommandType::kConversionPath: diff --git a/src/engine/src/coincentercommands.cpp b/src/engine/src/coincentercommands.cpp index aa7697fb..2f3327fe 100644 --- a/src/engine/src/coincentercommands.cpp +++ b/src/engine/src/coincentercommands.cpp @@ -51,6 +51,11 @@ void CoincenterCommands::addOption(const CoincenterCmdLineOptions &cmdLineOption _commands.emplace_back(CoincenterCommandType::kHealthCheck).setExchangeNames(optionParser.parseExchanges()); } + if (cmdLineOptions.currencies) { + optionParser = StringOptionParser(*cmdLineOptions.currencies); + _commands.emplace_back(CoincenterCommandType::kCurrencies).setExchangeNames(optionParser.parseExchanges()); + } + if (!cmdLineOptions.markets.empty()) { optionParser = StringOptionParser(cmdLineOptions.markets); _commands.push_back(CoincenterCommandFactory::CreateMarketCommand(optionParser)); diff --git a/src/engine/src/exchangesorchestrator.cpp b/src/engine/src/exchangesorchestrator.cpp index b115a71b..c59dd14a 100644 --- a/src/engine/src/exchangesorchestrator.cpp +++ b/src/engine/src/exchangesorchestrator.cpp @@ -290,6 +290,19 @@ ConversionPathPerExchange ExchangesOrchestrator::getConversionPaths(Market mk, E return conversionPathPerExchange; } +CurrenciesPerExchange ExchangesOrchestrator::getCurrenciesPerExchange(ExchangeNameSpan exchangeNames) { + log::info("Get all tradable currencies for {}", ConstructAccumulatedExchangeNames(exchangeNames)); + + UniquePublicSelectedExchanges selectedExchanges = _exchangeRetriever.selectOneAccount(exchangeNames); + + CurrenciesPerExchange ret(selectedExchanges.size()); + _threadPool.parallelTransform( + selectedExchanges.begin(), selectedExchanges.end(), ret.begin(), + [](Exchange *exchange) { return std::make_pair(exchange, exchange->queryTradableCurrencies()); }); + + return ret; +} + MarketsPerExchange ExchangesOrchestrator::getMarketsPerExchange(CurrencyCode cur1, CurrencyCode cur2, ExchangeNameSpan exchangeNames) { string curStr = cur1.str(); diff --git a/src/engine/src/queryresultprinter.cpp b/src/engine/src/queryresultprinter.cpp index 06f87101..a0718a0f 100644 --- a/src/engine/src/queryresultprinter.cpp +++ b/src/engine/src/queryresultprinter.cpp @@ -16,6 +16,7 @@ #include "cct_string.hpp" #include "coincentercommandtype.hpp" #include "currencycode.hpp" +#include "currencycodevector.hpp" #include "deposit.hpp" #include "depositsconstraints.hpp" #include "durationstring.hpp" @@ -68,6 +69,35 @@ json HealthCheckJson(const ExchangeHealthCheckStatus &healthCheckPerExchange) { return ToJson(CoincenterCommandType::kHealthCheck, std::move(in), std::move(out)); } +json CurrenciesJson(const CurrenciesPerExchange ¤ciesPerExchange) { + json in; + json inOpt; + in.emplace("opt", std::move(inOpt)); + + json out = json::object(); + for (const auto &[e, currencies] : currenciesPerExchange) { + json currenciesForExchange; + for (const CurrencyExchange &cur : currencies) { + json curExchangeJson; + + curExchangeJson.emplace("code", cur.standardCode().str()); + curExchangeJson.emplace("exchangeCode", cur.exchangeCode().str()); + curExchangeJson.emplace("altCode", cur.altCode().str()); + + curExchangeJson.emplace("canDeposit", cur.canDeposit()); + curExchangeJson.emplace("canWithdraw", cur.canWithdraw()); + curExchangeJson.emplace("canWithdraw", cur.canWithdraw()); + + curExchangeJson.emplace("isFiat", cur.isFiat()); + + currenciesForExchange.emplace_back(std::move(curExchangeJson)); + } + out.emplace(e->name(), std::move(currenciesForExchange)); + } + + return ToJson(CoincenterCommandType::kCurrencies, std::move(in), std::move(out)); +} + json MarketsJson(CurrencyCode cur1, CurrencyCode cur2, const MarketsPerExchange &marketsPerExchange) { json in; json inOpt; @@ -640,6 +670,95 @@ void QueryResultPrinter::printHealthCheck(const ExchangeHealthCheckStatus &healt logActivity(CoincenterCommandType::kHealthCheck, jsonData); } +void QueryResultPrinter::printCurrencies(const CurrenciesPerExchange ¤ciesPerExchange) const { + json jsonData = CurrenciesJson(currenciesPerExchange); + switch (_apiOutputType) { + case ApiOutputType::kFormattedTable: { + SimpleTable simpleTable("Currency", "Supported exchanges", "Exchange code(s)", "Alt code(s)", "Can deposit on", + "Can withdraw from", "Is fiat"); + // Compute all currencies for all exchanges + CurrencyCodeVector allCurrencyCodes; + + for (const auto &[_, currencies] : currenciesPerExchange) { + allCurrencyCodes.insert(allCurrencyCodes.end(), currencies.begin(), currencies.end()); + } + std::ranges::sort(allCurrencyCodes); + const auto [eraseIt1, eraseIt2] = std::ranges::unique(allCurrencyCodes); + allCurrencyCodes.erase(eraseIt1, eraseIt2); + + simpleTable.reserve(1U + allCurrencyCodes.size()); + + for (CurrencyCode cur : allCurrencyCodes) { + string supportedExchanges; + string exchangeCodes; + string altCodes; + string canDeposit; + string canWithdraw; + std::optional isFiat; + for (const auto &[exchange, currencies] : currenciesPerExchange) { + auto it = currencies.find(cur); + if (it != currencies.end()) { + // This exchange has this currency + if (!supportedExchanges.empty()) { + supportedExchanges.push_back(','); + } + supportedExchanges.append(exchange->name()); + if (!isFiat) { + isFiat = it->isFiat(); + } else if (*isFiat != it->isFiat()) { + log::error("Currency {} is fiat for an exchange and not fiat for another ('{}'), consider not fiat", cur, + exchange->name()); + isFiat = false; + } + if (cur != it->exchangeCode()) { + if (!exchangeCodes.empty()) { + exchangeCodes.push_back(','); + } + exchangeCodes.append(it->exchangeCode().str()); + exchangeCodes.push_back('['); + exchangeCodes.append(exchange->name()); + exchangeCodes.push_back(']'); + } + if (cur != it->altCode()) { + if (!exchangeCodes.empty()) { + exchangeCodes.push_back(','); + } + exchangeCodes.append(it->altCode().str()); + exchangeCodes.push_back('['); + exchangeCodes.append(exchange->name()); + exchangeCodes.push_back(']'); + } + if (it->canDeposit()) { + if (!canDeposit.empty()) { + canDeposit.push_back(','); + } + canDeposit.append(exchange->name()); + } + if (it->canWithdraw()) { + if (!canWithdraw.empty()) { + canWithdraw.push_back(','); + } + canWithdraw.append(exchange->name()); + } + } + } + + simpleTable.emplace_back(cur.str(), std::move(supportedExchanges), std::move(exchangeCodes), + std::move(altCodes), std::move(canDeposit), std::move(canWithdraw), isFiat.value()); + } + + printTable(simpleTable); + break; + } + case ApiOutputType::kJson: + printJson(jsonData); + break; + case ApiOutputType::kNoPrint: + break; + } + logActivity(CoincenterCommandType::kCurrencies, jsonData); +} + void QueryResultPrinter::printMarkets(CurrencyCode cur1, CurrencyCode cur2, const MarketsPerExchange &marketsPerExchange) const { json jsonData = MarketsJson(cur1, cur2, marketsPerExchange); diff --git a/src/objects/include/coincentercommandtype.hpp b/src/objects/include/coincentercommandtype.hpp index 3a1bc996..c0d790ac 100644 --- a/src/objects/include/coincentercommandtype.hpp +++ b/src/objects/include/coincentercommandtype.hpp @@ -6,6 +6,7 @@ namespace cct { enum class CoincenterCommandType : int8_t { kHealthCheck, + kCurrencies, kMarkets, kConversionPath, kLastPrice, diff --git a/src/objects/include/currencyexchangeflatset.hpp b/src/objects/include/currencyexchangeflatset.hpp index 5d9d9b7b..7a4a90da 100644 --- a/src/objects/include/currencyexchangeflatset.hpp +++ b/src/objects/include/currencyexchangeflatset.hpp @@ -53,9 +53,8 @@ class CurrencyExchangeFlatSet { const_iterator find(CurrencyCode standardCode) const { // This is possible as CurrencyExchanges are ordered by standard code - const_iterator lbIt = - std::lower_bound(_set.begin(), _set.end(), standardCode, - [](const CurrencyExchange &lhs, CurrencyCode c) { return lhs.standardCode() < c; }); + const_iterator lbIt = std::ranges::lower_bound( + _set, standardCode, [](const CurrencyExchange &lhs, CurrencyCode c) { return lhs.standardCode() < c; }); return lbIt == end() || standardCode < lbIt->standardCode() ? end() : lbIt; } diff --git a/src/objects/src/coincentercommandtype.cpp b/src/objects/src/coincentercommandtype.cpp index f12c24d4..830c25b7 100644 --- a/src/objects/src/coincentercommandtype.cpp +++ b/src/objects/src/coincentercommandtype.cpp @@ -9,6 +9,8 @@ std::string_view CoincenterCommandTypeToString(CoincenterCommandType type) { switch (type) { case CoincenterCommandType::kHealthCheck: return "HealthCheck"; + case CoincenterCommandType::kCurrencies: + return "Currencies"; case CoincenterCommandType::kMarkets: return "Markets"; case CoincenterCommandType::kConversionPath: @@ -57,6 +59,9 @@ CoincenterCommandType CoincenterCommandTypeFromString(std::string_view str) { if (str == "HealthCheck") { return CoincenterCommandType::kHealthCheck; } + if (str == "Currencies") { + return CoincenterCommandType::kCurrencies; + } if (str == "Markets") { return CoincenterCommandType::kMarkets; } diff --git a/src/tech/include/simpletable.hpp b/src/tech/include/simpletable.hpp index 5bdf1a40..23256d65 100644 --- a/src/tech/include/simpletable.hpp +++ b/src/tech/include/simpletable.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include #include @@ -26,8 +28,7 @@ class SimpleTable { class Row; /// Cell in a SimpleTable. - /// Can currently hold only 3 types of values: a string, a string_view or an integral type. - /// TODO: add support of 'double' if needed + /// Can currently hold only 4 types of values: a string, a string_view, a int64_t and a bool. class Cell { public: using IntegralType = int64_t; @@ -39,7 +40,7 @@ class SimpleTable { #else using string_type = string; #endif - using value_type = std::variant; + using value_type = std::variant; using size_type = uint32_t; explicit Cell(std::string_view v) : _data(v) {} @@ -54,7 +55,7 @@ class SimpleTable { explicit Cell(string_type &&v) : _data(std::move(v)) {} #endif - explicit Cell(IntegralType v) : _data(v) {} + explicit Cell(std::integral auto v) : _data(v) {} size_type size() const noexcept; @@ -62,9 +63,9 @@ class SimpleTable { using trivially_relocatable = is_trivially_relocatable::type; - bool operator==(const Cell &) const = default; + bool operator==(const Cell &) const noexcept = default; - auto operator<=>(const Cell &) const = default; + auto operator<=>(const Cell &) const noexcept = default; private: friend class Row; @@ -121,7 +122,7 @@ class SimpleTable { bool operator==(const Row &) const = default; - auto operator<=>(const Row &) const = default; + std::strong_ordering operator<=>(const Row &) const = default; private: friend class SimpleTable; diff --git a/src/tech/src/simpletable.cpp b/src/tech/src/simpletable.cpp index 1c019041..27a2a61b 100644 --- a/src/tech/src/simpletable.cpp +++ b/src/tech/src/simpletable.cpp @@ -21,6 +21,9 @@ const SimpleTable::Row SimpleTable::Row::kDivider; namespace { constexpr char kColumnSep = '|'; +constexpr std::string_view kBoolValueTrue = "yes"; +constexpr std::string_view kBoolValueFalse = "no"; + enum class AlignTo : int8_t { kLeft, kRight }; /// Helper function to align to left or right @@ -46,6 +49,8 @@ SimpleTable::size_type SimpleTable::Cell::size() const noexcept { return static_cast(std::get(_data).size()); case 2: return static_cast(nchars(std::get(_data))); + case 3: + return static_cast(std::get(_data) ? kBoolValueTrue.size() : kBoolValueFalse.size()); default: unreachable(); } @@ -63,6 +68,9 @@ void SimpleTable::Cell::print(std::ostream &os, size_type maxCellWidth) const { case 2: os << std::get(_data); break; + case 3: + os << (std::get(_data) ? kBoolValueTrue : kBoolValueFalse); + break; default: unreachable(); } diff --git a/src/tech/test/simpletable_test.cpp b/src/tech/test/simpletable_test.cpp index a5fd898e..c7ac4231 100644 --- a/src/tech/test/simpletable_test.cpp +++ b/src/tech/test/simpletable_test.cpp @@ -29,14 +29,17 @@ TEST(SimpleTable, SimplePrint) { SimpleTable::Row row1; row1.emplace_back("Amount"); row1.emplace_back("Currency"); + row1.emplace_back("Is Fiat"); table.push_back(std::move(row1)); SimpleTable::Row row2; row2.emplace_back("123.45"); row2.emplace_back("EUR"); + row2.emplace_back(true); table.push_back(std::move(row2)); SimpleTable::Row row3; row3.emplace_back(65); row3.emplace_back("BTC"); + row3.emplace_back(false); table.push_back(std::move(row3)); EXPECT_EQ(table.size(), 3U);