diff --git a/.github/workflows/clang-format-check.yml b/.github/workflows/clang-format-check.yml index bb271f51..9dad1f42 100644 --- a/.github/workflows/clang-format-check.yml +++ b/.github/workflows/clang-format-check.yml @@ -9,7 +9,7 @@ on: jobs: formatting-check: name: Formatting Check - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Run clang-format style check for C/C++ programs. diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cf5a4cd3..e95706ec 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -20,7 +20,7 @@ on: jobs: analyze: name: Analyze - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: actions: read diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8c446b2d..06d406c6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -9,7 +9,7 @@ on: jobs: docker: name: Docker build - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: with-tests: [0, 1] diff --git a/.github/workflows/ubuntu-special.yml b/.github/workflows/ubuntu-special.yml index bca06e55..a6dae2fa 100644 --- a/.github/workflows/ubuntu-special.yml +++ b/.github/workflows/ubuntu-special.yml @@ -9,10 +9,9 @@ on: jobs: ubuntu-special-build: name: Build on Ubuntu with monitoring / protobuf support - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - compiler: [g++-11] buildmode: [Debug] build-special-from-source: [0, 1] prometheus-options: ["-DBUILD_SHARED_LIBS=ON -DENABLE_PULL=OFF -DENABLE_PUSH=ON -DENABLE_COMPRESSION=OFF -DENABLE_TESTING=OFF"] @@ -25,7 +24,6 @@ jobs: run: | sudo apt update sudo apt install cmake libssl-dev git libcurl4-openssl-dev ninja-build -y --no-install-recommends - echo "CC=$(echo ${{matrix.compiler}} | sed -e 's/^g++/gcc/' | sed 's/+//g')" >> $GITHUB_ENV - name: Install prometheus-cpp run: | @@ -39,14 +37,12 @@ jobs: sudo cmake --install _build env: - CXX: ${{matrix.compiler}} CMAKE_BUILD_TYPE: ${{matrix.buildmode}} if: matrix.build-special-from-source == 0 - name: Configure CMake run: cmake -S . -B build -DCCT_BUILD_PROMETHEUS_FROM_SRC=${{matrix.build-special-from-source}} -DCCT_ENABLE_PROTO=${{matrix.build-special-from-source}} -DCCT_ENABLE_ASAN=OFF -GNinja env: - CXX: ${{matrix.compiler}} CMAKE_BUILD_TYPE: ${{matrix.buildmode}} - name: Build diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 95413de2..7b0a0c41 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -9,10 +9,10 @@ on: jobs: ubuntu-build: name: Build on Ubuntu - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: matrix: - compiler: [g++-11, clang++-18] + compiler: [g++-13, clang++-18] buildmode: [Debug, Release] steps: @@ -25,14 +25,6 @@ jobs: sudo apt install cmake libssl-dev libcurl4-openssl-dev ninja-build -y --no-install-recommends echo "CC=$(echo ${{matrix.compiler}} | sed -e 's/^g++/gcc/' | sed 's/+//g')" >> $GITHUB_ENV - - name: Install gcc - run: | - sudo apt install ${{matrix.compiler}} -y --no-install-recommends - - # Temporary workaround for libasan bug stated here: https://github.com/google/sanitizers/issues/1716 - sudo sysctl vm.mmap_rnd_bits=28 - if: startsWith(matrix.compiler, 'g') - - name: Install clang run: | CLANG_VERSION=$(echo ${{matrix.compiler}} | cut -d- -f2) diff --git a/CMakeLists.txt b/CMakeLists.txt index 665b7821..0e38c8e6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -99,7 +99,7 @@ if(CCT_ENABLE_TESTS) enable_testing() endif() -# nlohmann_json - json library +# nlohmann_json - json container library find_package(nlohmann_json CONFIG) if(NOT nlohmann_json_FOUND) FetchContent_Declare( @@ -111,6 +111,18 @@ if(NOT nlohmann_json_FOUND) list(APPEND fetchContentPackagesToMakeAvailable nlohmann_json) endif() +# Glaze - fast json serialization library +find_package(glaze CONFIG) +if(NOT glaze) + FetchContent_Declare( + glaze + URL https://github.com/stephenberry/glaze/archive/refs/tags/v3.6.2.tar.gz + URL_HASH SHA256=74b14656b7a47c0a03d0a857adf5059e8c2351a7a84623593be0dd16b293216c + ) + + list(APPEND fetchContentPackagesToMakeAvailable glaze) +endif() + # prometheus for monitoring support if(CCT_BUILD_PROMETHEUS_FROM_SRC) FetchContent_Declare( @@ -259,6 +271,8 @@ endif() # Link to sub folders CMakeLists.txt, from the lowest level to the highest level for documentation # (beware of cyclic dependencies) add_subdirectory(src/tech) +add_subdirectory(src/basic-objects) +add_subdirectory(src/schema) add_subdirectory(src/monitoring) add_subdirectory(src/http-request) add_subdirectory(src/objects) diff --git a/INSTALL.md b/INSTALL.md index dfe33fc2..59418c7c 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -54,7 +54,7 @@ But they have partial support which is sufficient to build `coincenter`. The following compilers are known to compile `coincenter` (and are tested in the CI): -- **GCC** version >= 11 +- **GCC** version >= 12 - **Clang** version >= 18 - **MSVC** version >= 19.39 @@ -135,14 +135,15 @@ cmake -S . -B build --preset conan-release In all cases, they do not need to be installed. If they are not found at configure time, `cmake` will fetch sources and compile them automatically. If you are building frequently `coincenter` you can install them to speed up its compilation. -| Library | Description | License | -| -------------------------------------------------------------- | -------------------------------------------------- | -------------------- | -| [amc](https://github.com/AmadeusITGroup/amc.git) | High performance C++ containers (maintained by me) | MIT | -| [googletest](https://github.com/google/googletest.git) | Google Testing and Mocking Framework | BSD-3-Clause License | -| [json](https://github.com/nlohmann/json) | JSON for Modern C++ | MIT | -| [spdlog](https://github.com/gabime/spdlog.git) | Fast C++ logging library | MIT | -| [prometheus-cpp](https://github.com/jupp0r/prometheus-cpp.git) | Prometheus Client Library for Modern C++ | MIT | -| [jwt-cpp](https://github.com/Thalhammer/jwt-cpp) | Creating and validating json web tokens in C++ | MIT | +| Library | Description | License | +| -------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------- | +| [amc](https://github.com/AmadeusITGroup/amc.git) | High performance C++ containers (maintained by me) | MIT | +| [googletest](https://github.com/google/googletest.git) | Google Testing and Mocking Framework | BSD-3-Clause License | +| [json container](https://github.com/nlohmann/json) | JSON for Modern C++ | MIT | +| [json serialization](https://github.com/stephenberry/glaze) | Extremely fast, in memory, JSON and interface library for modern C++ | MIT | +| [spdlog](https://github.com/gabime/spdlog.git) | Fast C++ logging library | MIT | +| [prometheus-cpp](https://github.com/jupp0r/prometheus-cpp.git) | Prometheus Client Library for Modern C++ | MIT | +| [jwt-cpp](https://github.com/Thalhammer/jwt-cpp) | Creating and validating json web tokens in C++ | MIT | ### With cmake diff --git a/src/api/exchanges/src/kucoinprivateapi.cpp b/src/api/exchanges/src/kucoinprivateapi.cpp index 26cee369..58f739ba 100644 --- a/src/api/exchanges/src/kucoinprivateapi.cpp +++ b/src/api/exchanges/src/kucoinprivateapi.cpp @@ -72,7 +72,7 @@ json PrivateQuery(CurlHandle& curlHandle, const APIKey& apiKey, HttpRequestType strToSign.push_back('?'); strToSign.append(postData.str()); } else { - strToSign.append(postData.toJson().dump()); + strToSign.append(postData.toJsonStr()); postDataFormat = CurlOptions::PostDataFormat::kJson; } } diff --git a/src/basic-objects/CMakeLists.txt b/src/basic-objects/CMakeLists.txt new file mode 100644 index 00000000..92247654 --- /dev/null +++ b/src/basic-objects/CMakeLists.txt @@ -0,0 +1,33 @@ +# Create objects lib +aux_source_directory(src BASIC_OBJECTS_SRC) + +add_coincenter_library(basic-objects STATIC ${BASIC_OBJECTS_SRC}) + +target_link_libraries(coincenter_basic-objects PUBLIC coincenter_tech) + +add_unit_test( + currencycode_test + test/currencycode_test.cpp + LIBRARIES + coincenter_tech + DEFINITIONS + CCT_DISABLE_SPDLOG +) + +add_unit_test( + market_test + test/market_test.cpp + LIBRARIES + coincenter_basic-objects + DEFINITIONS + CCT_DISABLE_SPDLOG +) + +add_unit_test( + monetaryamount_test + test/monetaryamount_test.cpp + LIBRARIES + coincenter_basic-objects + DEFINITIONS + CCT_DISABLE_SPDLOG +) \ No newline at end of file diff --git a/src/objects/include/currencycode.hpp b/src/basic-objects/include/currencycode.hpp similarity index 62% rename from src/objects/include/currencycode.hpp rename to src/basic-objects/include/currencycode.hpp index 2f56794f..7079e805 100644 --- a/src/objects/include/currencycode.hpp +++ b/src/basic-objects/include/currencycode.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include #include @@ -11,13 +13,14 @@ #include "cct_format.hpp" #include "cct_hash.hpp" #include "cct_invalid_argument_exception.hpp" +#include "cct_json-serialization.hpp" #include "cct_string.hpp" #include "toupperlower.hpp" namespace cct { struct CurrencyCodeBase { - static constexpr int kMaxLen = 10; + static constexpr uint32_t kMaxLen = 10; static constexpr uint64_t kNbBitsChar = 6; static constexpr uint64_t kNbBitsNbDecimals = 4; @@ -25,39 +28,64 @@ struct CurrencyCodeBase { static constexpr uint64_t kNbDecimals4Mask = (1ULL << kNbBitsNbDecimals) - 1ULL; static constexpr uint64_t kNbDecimals6Mask = (1ULL << 6ULL) - 1ULL; - static constexpr uint64_t kFirstCharMask = ~((1ULL << (kNbBitsNbDecimals + (kMaxLen - 1) * kNbBitsChar)) - 1ULL); + static constexpr uint64_t kFirstCharMask = ~((1ULL << (kNbBitsNbDecimals + (kMaxLen - 1U) * kNbBitsChar)) - 1ULL); - static constexpr uint64_t kBeforeLastCharMask = kFirstCharMask >> (kNbBitsChar * (kMaxLen - 2)); + static constexpr uint64_t NCharMask(uint64_t n) noexcept { + if (n == 1) { + return kFirstCharMask; + } + return NCharMask(n - 1) + (kFirstCharMask >> (kNbBitsChar * (n - 1))); + } + + static constexpr auto ComputeAllCharMasks() { + std::array allCharMasks; + allCharMasks[0] = 0; + for (std::remove_const_t sz = 1; sz <= kMaxLen; ++sz) { + allCharMasks[sz] = NCharMask(sz); + } + return allCharMasks; + } + + static constexpr uint64_t kBeforeLastCharMask = kFirstCharMask >> (kNbBitsChar * (kMaxLen - 2U)); static constexpr int64_t kMaxNbDecimalsLongCurrencyCode = 15; // 2^4 - 1 static constexpr char kFirstAuthorizedLetter = 32; // ' ' static constexpr char kLastAuthorizedLetter = 95; // '_' - static constexpr char CharAt(uint64_t data, int pos) noexcept { - return static_cast((data >> (kNbBitsNbDecimals + kNbBitsChar * (kMaxLen - pos - 1))) & + static constexpr char CharAt(uint64_t data, uint32_t pos) noexcept { + return static_cast((data >> (kNbBitsNbDecimals + kNbBitsChar * (kMaxLen - pos - 1U))) & ((1ULL << kNbBitsChar) - 1ULL)) + kFirstAuthorizedLetter; } + static constexpr void ValidateChar(char &ch) { + if (ch >= 'a') { + if (ch > 'z') { + throw invalid_argument("Unexpected char '{}' in currency acronym", ch); + } + ch -= 'a' - 'A'; + } else if (ch <= kFirstAuthorizedLetter || ch > kLastAuthorizedLetter) { + throw invalid_argument("Unexpected char '{}' in currency acronym", ch); + } + } + + static constexpr uint64_t GetCharAtPosBmp(char ch, uint32_t charPos) { + return static_cast(ch - kFirstAuthorizedLetter) + << (kNbBitsNbDecimals + kNbBitsChar * (kMaxLen - 1U - charPos)); + } + static constexpr uint64_t DecimalsMask(bool isLongCurrencyCode) noexcept { return isLongCurrencyCode ? kNbDecimals4Mask : kNbDecimals6Mask; } static constexpr uint64_t StrToBmp(std::string_view acronym) { - uint64_t ret = 0; - uint32_t charPos = kMaxLen; + uint64_t ret{}; + uint32_t charPos{}; for (char ch : acronym) { - if (ch >= 'a') { - if (ch > 'z') { - throw invalid_argument("Unexpected char '{}' in acronym '{}'", ch, acronym); - } - ch -= 'a' - 'A'; - } else if (ch <= kFirstAuthorizedLetter || ch > kLastAuthorizedLetter) { - throw invalid_argument("Unexpected char '{}' in acronym '{}'", ch, acronym); - } - - ret |= static_cast(ch - kFirstAuthorizedLetter) << (kNbBitsNbDecimals + kNbBitsChar * --charPos); + ValidateChar(ch); + ret |= GetCharAtPosBmp(ch, charPos); + ++charPos; } return ret; } @@ -122,8 +150,9 @@ class CurrencyCode { public: using iterator = CurrencyCodeIterator; using const_iterator = iterator; + using size_type = uint32_t; - static constexpr auto kMaxLen = CurrencyCodeBase::kMaxLen; + static constexpr size_type kMaxLen = CurrencyCodeBase::kMaxLen; /// Returns true if and only if a CurrencyCode can be constructed from 'curStr'. /// Note that an empty string is a valid representation of a CurrencyCode. @@ -138,7 +167,7 @@ class CurrencyCode { constexpr CurrencyCode() noexcept : _data() {} /// Constructs a currency code from a char array. - template = true> + template = true> constexpr CurrencyCode(const char (&acronym)[N]) : _data(CurrencyCodeBase::StrToBmp(acronym)) {} /// Constructs a currency code from given string. @@ -151,19 +180,52 @@ class CurrencyCode { _data = CurrencyCodeBase::StrToBmp(acronym); } - constexpr const_iterator begin() const { return const_iterator(_data); } - constexpr const_iterator end() const { return const_iterator(_data, size()); } + /// Constructs a currency code from 'sz' chars, all set to 'ch'. + constexpr CurrencyCode(size_type sz, char ch) : _data() { resize(sz, ch); } + + constexpr const_iterator begin() const noexcept { return const_iterator(_data); } + constexpr const_iterator end() const noexcept { return const_iterator(_data, size()); } + + constexpr const_iterator cbegin() const noexcept { return begin(); } + constexpr const_iterator cend() const noexcept { return end(); } + + constexpr size_type size() const noexcept { + size_type count = kMaxLen; + size_type first{}; + while (count != 0) { + size_type step = count / 2; + size_type pos = first + step; + if ((_data & (CurrencyCodeBase::kFirstCharMask >> (CurrencyCodeBase::kNbBitsChar * pos))) != 0) { + // char is present at position 'step', so the size is at least 'pos' + first = pos + 1; + count -= step + 1; + } else { + count = step; + } + } + return first; + } + + constexpr void resize(size_type newSize, char ch) { + auto sz = size(); + if (sz < newSize) { + if (newSize > kMaxLen) { + throw invalid_argument("Cannot resize CurrencyCode to size {} > {}", newSize, kMaxLen); + } + + CurrencyCodeBase::ValidateChar(ch); - constexpr uint64_t size() const { - uint64_t sz = 0; - while (static_cast(sz) < kMaxLen && - (_data & (CurrencyCodeBase::kFirstCharMask >> (CurrencyCodeBase::kNbBitsChar * sz)))) { - ++sz; + for (size_type charPos = sz; charPos < newSize; ++charPos) { + _data |= CurrencyCodeBase::GetCharAtPosBmp(ch, charPos); + } + } else if (sz > newSize) { + _data &= kCharMaskArrayByLen[newSize] + CurrencyCodeBase::DecimalsMask(isLongCurrencyCode()); } - return sz; } - constexpr uint64_t length() const { return size(); } + constexpr void assign(const char *buf, size_type sz) { *this = CurrencyCode(std::string_view(buf, sz)); } + + constexpr size_type length() const noexcept { return size(); } /// Get a string of this CurrencyCode, trimmed. string str() const { @@ -178,7 +240,7 @@ class CurrencyCode { if (curStr.size() > kMaxLen) { return false; } - for (uint32_t charPos = 0; charPos < kMaxLen; ++charPos) { + for (size_type charPos = 0; charPos < kMaxLen; ++charPos) { const char ch = (*this)[charPos]; if (ch == CurrencyCodeBase::kFirstAuthorizedLetter) { return curStr.size() == charPos; @@ -191,16 +253,17 @@ class CurrencyCode { } /// Append currency string representation to given string. - void appendStrTo(string &str) const { + template + void appendStrTo(StringT &str) const { const auto len = size(); str.append(len, '\0'); - append(str.end() - len); + appendTo(str.end() - len); } /// Append currency string representation to given output iterator template - constexpr OutputIt append(OutputIt it) const { - for (uint32_t charPos = 0; charPos < kMaxLen; ++charPos) { + constexpr OutputIt appendTo(OutputIt it) const { + for (size_type charPos = 0; charPos < kMaxLen; ++charPos) { const char ch = (*this)[charPos]; if (ch == CurrencyCodeBase::kFirstAuthorizedLetter) { break; @@ -214,11 +277,13 @@ class CurrencyCode { /// Returns a 64 bits code constexpr uint64_t code() const noexcept { return _data; } - constexpr bool isDefined() const noexcept { return static_cast(_data & CurrencyCodeBase::kFirstCharMask); } + constexpr bool isDefined() const noexcept { return (_data & CurrencyCodeBase::kFirstCharMask) != 0; } constexpr bool isNeutral() const noexcept { return !isDefined(); } - constexpr char operator[](uint32_t pos) const { return CurrencyCodeBase::CharAt(_data, static_cast(pos)); } + constexpr char operator[](uint32_t pos) const noexcept { + return CurrencyCodeBase::CharAt(_data, static_cast(pos)); + } /// Note that this respects the lexicographical order - chars are encoded from the most significant bits first constexpr std::strong_ordering operator<=>(const CurrencyCode &) const noexcept = default; @@ -226,7 +291,7 @@ class CurrencyCode { constexpr bool operator==(const CurrencyCode &) const noexcept = default; friend std::ostream &operator<<(std::ostream &os, const CurrencyCode &cur) { - for (uint32_t charPos = 0; charPos < kMaxLen; ++charPos) { + for (size_type charPos = 0; charPos < kMaxLen; ++charPos) { const char ch = cur[charPos]; if (ch == CurrencyCodeBase::kFirstAuthorizedLetter) { break; @@ -240,6 +305,8 @@ class CurrencyCode { friend class Market; friend class MonetaryAmount; + static constexpr auto kCharMaskArrayByLen = CurrencyCodeBase::ComputeAllCharMasks(); + // bitmap with 10 words of 6 bits (from ascii [33, 95]) + 4 extra bits that will be used by // MonetaryAmount to hold number of decimals (max 15) // Example, with currency code "EUR": @@ -276,7 +343,7 @@ class CurrencyCode { void appendStrWithSpaceTo(string &str) const { const auto len = size(); str.append(len + 1UL, ' '); - append(str.end() - len); + appendTo(str.end() - len); } }; @@ -298,7 +365,7 @@ struct fmt::formatter { template auto format(const cct::CurrencyCode &cur, FormatContext &ctx) const -> decltype(ctx.out()) { - return cur.append(ctx.out()); + return cur.appendTo(ctx.out()); } }; #endif @@ -306,7 +373,43 @@ struct fmt::formatter { // Specialize std::hash for easy usage of CurrencyCode as unordered_map key namespace std { template <> -struct hash { - auto operator()(const cct::CurrencyCode ¤cyCode) const { return cct::HashValue64(currencyCode.code()); } +struct hash<::cct::CurrencyCode> { + auto operator()(const ::cct::CurrencyCode ¤cyCode) const { return ::cct::HashValue64(currencyCode.code()); } }; } // namespace std + +namespace glz::detail { +template <> +struct from { + template + static void op(auto &&value, is_context auto &&, It &&it, End &&end) noexcept { + // used as a value. As a key, the first quote will not be present. + auto endIt = std::find(*it == '"' ? ++it : it, end, '"'); + value = std::string_view(it, endIt); + it = ++endIt; + } +}; + +template <> +struct to { + template + static void op(auto &&value, Ctx &&, B &&b, IX &&ix) { + auto valueLen = value.size(); + bool inQuotes = ix != 0 && b[ix - 1] == ':'; + int64_t additionalSize = (inQuotes ? 2L : 0L) + static_cast(ix) + static_cast(valueLen) - + static_cast(b.size()); + if (additionalSize > 0) { + b.append(additionalSize, ' '); + } + + if (inQuotes) { + b[ix++] = '"'; + } + value.appendTo(b.data() + ix); + ix += valueLen; + if (inQuotes) { + b[ix++] = '"'; + } + } +}; +} // namespace glz::detail diff --git a/src/objects/include/currencycodeset.hpp b/src/basic-objects/include/currencycodeset.hpp similarity index 100% rename from src/objects/include/currencycodeset.hpp rename to src/basic-objects/include/currencycodeset.hpp diff --git a/src/objects/include/currencycodevector.hpp b/src/basic-objects/include/currencycodevector.hpp similarity index 100% rename from src/objects/include/currencycodevector.hpp rename to src/basic-objects/include/currencycodevector.hpp diff --git a/src/objects/include/file.hpp b/src/basic-objects/include/file.hpp similarity index 100% rename from src/objects/include/file.hpp rename to src/basic-objects/include/file.hpp diff --git a/src/objects/include/market-vector.hpp b/src/basic-objects/include/market-vector.hpp similarity index 100% rename from src/objects/include/market-vector.hpp rename to src/basic-objects/include/market-vector.hpp diff --git a/src/basic-objects/include/market.hpp b/src/basic-objects/include/market.hpp new file mode 100644 index 00000000..b25f31cb --- /dev/null +++ b/src/basic-objects/include/market.hpp @@ -0,0 +1,192 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "cct_format.hpp" +#include "cct_json-serialization.hpp" +#include "cct_string.hpp" +#include "currencycode.hpp" + +namespace cct { + +struct MarketBase { + struct StringOutputConfig { + char currencyCodeSep = '-'; + bool lowerCase = false; + }; +}; + +/// Represents a tradable market from a currency pair. +/// Could be a fiat / coin or a coin / coin couple (fiat / fiat couple is possible but probably not relevant). +/// Important note: BTC/ETH != ETH/BTC. Use reverse() to reverse it. +class Market { + public: + enum class Type : int8_t { kRegularExchangeMarket, kFiatConversionMarket }; + + constexpr Market() noexcept(std::is_nothrow_default_constructible_v) = default; + + constexpr Market(CurrencyCode first, CurrencyCode second, Type type = Type::kRegularExchangeMarket) + : _assets({first, second}) { + setType(type); + } + + /// Create a Market from its string representation. + /// The two currency codes must be separated by given char separator. + explicit Market(std::string_view marketStrRep, char currencyCodeSep = '-', Type type = Type::kRegularExchangeMarket); + + bool isDefined() const { return base().isDefined() && quote().isDefined(); } + + bool isNeutral() const { return base().isNeutral() && quote().isNeutral(); } + + /// Computes the reverse market. + /// Example: return XRP/BTC for a market BTC/XRP + [[nodiscard]] Market reverse() const { return {_assets[1], _assets[0]}; } + + /// Get the base CurrencyCode of this Market. + CurrencyCode base() const { return _assets[0]; } + + /// Get the quote CurrencyCode of this Market. + CurrencyCode quote() const { return _assets[1]; } + + /// Get the string length representation of this Market. + uint32_t strLen(bool withSep = true) const noexcept { + return base().size() + quote().size() + static_cast(type() == Type::kFiatConversionMarket) + + static_cast(withSep); + } + + /// Given 'c' a currency traded in this Market, return the other currency it is paired with. + /// If 'c' is not traded by this market, return the second currency. + [[nodiscard]] CurrencyCode opposite(CurrencyCode cur) const { return _assets[1] == cur ? _assets[0] : _assets[1]; } + + /// Tells whether this market trades given currency code. + bool canTrade(CurrencyCode cur) const { return cur == base() || cur == quote(); } + + constexpr auto operator<=>(const Market &) const noexcept = default; + + string str() const { return assetsPairStrUpper('-'); } + + Type type() const noexcept { return static_cast(_assets[0].getAdditionalBits()); } + + friend std::ostream &operator<<(std::ostream &os, const Market &mk); + + /// Returns a string representing this Market in lower case + string assetsPairStrLower(char sep = '\0') const { + return assetsPairStr(MarketBase::StringOutputConfig{.currencyCodeSep = sep, .lowerCase = true}); + } + + /// Returns a string representing this Market in upper case + string assetsPairStrUpper(char sep = '\0') const { + return assetsPairStr(MarketBase::StringOutputConfig{.currencyCodeSep = sep, .lowerCase = false}); + } + + /// Append market string representation to given string. + template + void appendStrTo(StringT &str, + MarketBase::StringOutputConfig stringOutputConfig = MarketBase::StringOutputConfig{}) const { + const auto len = strLen(stringOutputConfig.currencyCodeSep != '\0'); + str.append(len, '\0'); + appendTo(str.end() - len, stringOutputConfig); + } + + /// Append currency string representation to given output iterator + template + constexpr OutputIt appendTo( + OutputIt it, MarketBase::StringOutputConfig stringOutputConfig = MarketBase::StringOutputConfig{}) const { + if (type() == Type::kFiatConversionMarket) { + *it = '*'; + ++it; + } + auto begIt = it; + it = base().appendTo(it); + if (stringOutputConfig.currencyCodeSep != '\0') { + *it = stringOutputConfig.currencyCodeSep; + ++it; + } + it = quote().appendTo(it); + if (stringOutputConfig.lowerCase) { + std::transform(begIt, it, begIt, tolower); + } + return it; + } + + private: + string assetsPairStr(MarketBase::StringOutputConfig stringOutputConfig) const { + string ret(strLen(stringOutputConfig.currencyCodeSep != '\0'), '\0'); + appendTo(ret.begin(), stringOutputConfig); + return ret; + } + + constexpr void setType(Type type) { _assets[0].uncheckedSetAdditionalBits(static_cast(type)); } + + std::array _assets; +}; + +} // namespace cct + +#ifndef CCT_DISABLE_SPDLOG +template <> +struct fmt::formatter<::cct::Market> { + constexpr auto parse(format_parse_context &ctx) -> decltype(ctx.begin()) { + auto it = ctx.begin(); + const auto end = ctx.end(); + if (it != end && *it != '}') { + throw format_error("invalid format"); + } + return it; + } + + template + auto format(const ::cct::Market &mk, FormatContext &ctx) const -> decltype(ctx.out()) { + return fmt::format_to(ctx.out(), "{}-{}", mk.base(), mk.quote()); + } +}; +#endif + +namespace std { +template <> +struct hash<::cct::Market> { + auto operator()(const ::cct::Market &mk) const { + return ::cct::HashCombine(hash<::cct::CurrencyCode>()(mk.base()), hash<::cct::CurrencyCode>()(mk.quote())); + } +}; +} // namespace std + +namespace glz::detail { +template <> +struct from { + template + static void op(auto &&value, is_context auto &&, It &&it, End &&end) noexcept { + // used as a value. As a key, the first quote will not be present. + auto endIt = std::find(*it == '"' ? ++it : it, end, '"'); + value = std::string_view(it, endIt); + it = ++endIt; + } +}; + +template <> +struct to { + template + static void op(auto &&value, Ctx &&, B &&b, IX &&ix) { + auto valueLen = value.size(); + bool inQuotes = ix != 0 && b[ix - 1] == ':'; + int64_t additionalSize = (inQuotes ? 2L : 0L) + static_cast(ix) + static_cast(valueLen) - + static_cast(b.size()); + if (additionalSize > 0) { + b.append(additionalSize, ' '); + } + + if (inQuotes) { + b[ix++] = '"'; + } + value.appendTo(b.data() + ix); + ix += valueLen; + if (inQuotes) { + b[ix++] = '"'; + } + } +}; +} // namespace glz::detail diff --git a/src/objects/include/monetary-amount-vector.hpp b/src/basic-objects/include/monetary-amount-vector.hpp similarity index 100% rename from src/objects/include/monetary-amount-vector.hpp rename to src/basic-objects/include/monetary-amount-vector.hpp diff --git a/src/objects/include/monetaryamount.hpp b/src/basic-objects/include/monetaryamount.hpp similarity index 91% rename from src/objects/include/monetaryamount.hpp rename to src/basic-objects/include/monetaryamount.hpp index f8969fde..8522ccfe 100644 --- a/src/objects/include/monetaryamount.hpp +++ b/src/basic-objects/include/monetaryamount.hpp @@ -12,6 +12,7 @@ #include "cct_format.hpp" #include "cct_hash.hpp" +#include "cct_json-serialization.hpp" #include "cct_log.hpp" #include "cct_string.hpp" #include "currencycode.hpp" @@ -46,6 +47,8 @@ class MonetaryAmount { enum class RoundType : int8_t { kDown, kUp, kNearest }; + static constexpr std::size_t kMaxNbCharsAmount = std::numeric_limits::digits10 + 3; + /// Constructs a MonetaryAmount with a value of 0 of neutral currency. constexpr MonetaryAmount() noexcept : _amount(0) {} @@ -107,6 +110,11 @@ class MonetaryAmount { /// Example: "5.6235" with 6 decimals will return 5623500 [[nodiscard]] AmountType amount() const { return _amount; } + enum class WithSpaces : int8_t { kNo, kYes }; + + // Get the size of the string representation of this MonetaryAmount. + [[nodiscard]] uint32_t strLen(WithSpaces withSpace = WithSpaces::kYes) const; + /// Get an integral representation of this MonetaryAmount multiplied by given number of decimals. /// If an overflow would occur for the resulting amount, return std::nullopt /// Example: "5.6235" with 6 decimals will return 5623500 @@ -300,11 +308,11 @@ class MonetaryAmount { /// kMaxNbCharsAmount for the amount (explanation above) /// + CurrencyCodeBase::kMaxLen + 1 for the currency and the space separator template - OutputIt append(OutputIt it) const { + OutputIt appendTo(OutputIt it) const { it = appendAmount(it); if (!_curWithDecimals.isNeutral()) { *it = ' '; - it = _curWithDecimals.append(++it); + it = _curWithDecimals.appendTo(++it); } return it; } @@ -334,7 +342,7 @@ class MonetaryAmount { appendCurrencyStr(str); } - [[nodiscard]] uint64_t code() const noexcept { + [[nodiscard]] uint64_t hashCode() const noexcept { return HashCombine(static_cast(_amount), static_cast(_curWithDecimals.code())); } @@ -344,7 +352,6 @@ class MonetaryAmount { using UnsignedAmountType = uint64_t; static constexpr AmountType kMaxAmountFullNDigits = ipow10(std::numeric_limits::digits10); - static constexpr std::size_t kMaxNbCharsAmount = std::numeric_limits::digits10 + 3; void appendCurrencyStr(string &str) const { if (!_curWithDecimals.isNeutral()) { @@ -415,14 +422,51 @@ struct fmt::formatter { template auto format(const cct::MonetaryAmount &ma, FormatContext &ctx) const -> decltype(ctx.out()) { - return ma.append(ctx.out()); + return ma.appendTo(ctx.out()); } }; #endif +// Specialize std::hash for easy usage of MonetaryAmount as unordered_map key namespace std { template <> -struct hash { - auto operator()(const cct::MonetaryAmount &monetaryAmount) const { return monetaryAmount.code(); } +struct hash<::cct::MonetaryAmount> { + auto operator()(const ::cct::MonetaryAmount &monetaryAmount) const { return monetaryAmount.hashCode(); } }; } // namespace std + +namespace glz::detail { +template <> +struct from { + template + static void op(auto &&value, is_context auto &&, It &&it, End &&end) noexcept { + // used as a value. As a key, the first quote will not be present. + auto endIt = std::find(*it == '"' ? ++it : it, end, '"'); + value = ::cct::MonetaryAmount(std::string_view(it, endIt)); + it = ++endIt; + } +}; + +template <> +struct to { + template + static void op(auto &&value, Ctx &&, B &&b, IX &&ix) { + auto valueLen = value.strLen(); + bool inQuotes = ix != 0 && b[ix - 1] == ':'; + int64_t additionalSize = (inQuotes ? 2L : 0L) + static_cast(ix) + static_cast(valueLen) - + static_cast(b.size()); + if (additionalSize > 0) { + b.append(additionalSize, ' '); + } + + if (inQuotes) { + b[ix++] = '"'; + } + value.appendTo(b.data() + ix); + ix += valueLen; + if (inQuotes) { + b[ix++] = '"'; + } + } +}; +} // namespace glz::detail \ No newline at end of file diff --git a/src/objects/include/reader.hpp b/src/basic-objects/include/reader.hpp similarity index 100% rename from src/objects/include/reader.hpp rename to src/basic-objects/include/reader.hpp diff --git a/src/objects/include/writer.hpp b/src/basic-objects/include/writer.hpp similarity index 100% rename from src/objects/include/writer.hpp rename to src/basic-objects/include/writer.hpp diff --git a/src/objects/src/file.cpp b/src/basic-objects/src/file.cpp similarity index 100% rename from src/objects/src/file.cpp rename to src/basic-objects/src/file.cpp diff --git a/src/objects/src/market.cpp b/src/basic-objects/src/market.cpp similarity index 69% rename from src/objects/src/market.cpp rename to src/basic-objects/src/market.cpp index 40d806ec..a937d083 100644 --- a/src/objects/src/market.cpp +++ b/src/basic-objects/src/market.cpp @@ -25,28 +25,6 @@ Market::Market(std::string_view marketStrRep, char currencyCodeSep, Type type) { setType(type); } -string Market::assetsPairStr(char sep, bool lowerCase) const { - string ret; - switch (type()) { - case Type::kRegularExchangeMarket: - break; - case Type::kFiatConversionMarket: - ret.push_back('*'); - break; - default: - unreachable(); - } - base().appendStrTo(ret); - if (sep != 0) { - ret.push_back(sep); - } - quote().appendStrTo(ret); - if (lowerCase) { - std::ranges::transform(ret, ret.begin(), tolower); - } - return ret; -} - std::ostream& operator<<(std::ostream& os, const Market& mk) { os << mk.str(); return os; diff --git a/src/objects/src/monetaryamount.cpp b/src/basic-objects/src/monetaryamount.cpp similarity index 97% rename from src/objects/src/monetaryamount.cpp rename to src/basic-objects/src/monetaryamount.cpp index 4c6dca7f..7f6e0966 100644 --- a/src/objects/src/monetaryamount.cpp +++ b/src/basic-objects/src/monetaryamount.cpp @@ -228,6 +228,16 @@ MonetaryAmount::MonetaryAmount(double amount, CurrencyCode currencyCode, RoundTy round(nbDecimals, roundType); } +[[nodiscard]] uint32_t MonetaryAmount::strLen(WithSpaces withSpace) const { + const auto nbDigitsAmount = ndigits(_amount); + const auto szCurrencyCode = _curWithDecimals.size(); + const auto nbDec = nbDecimals(); + return static_cast(static_cast(_amount < 0) + nbDigitsAmount + static_cast(nbDec != 0) + + static_cast(nbDec >= nbDigitsAmount) + + static_cast(withSpace == WithSpaces::kYes && szCurrencyCode != 0) + + szCurrencyCode); +} + std::optional MonetaryAmount::amount(int8_t nbDecimals) const { AmountType integralAmount = _amount; const int8_t ourNbDecimals = this->nbDecimals(); diff --git a/src/objects/src/reader.cpp b/src/basic-objects/src/reader.cpp similarity index 100% rename from src/objects/src/reader.cpp rename to src/basic-objects/src/reader.cpp diff --git a/src/objects/test/currencycode_test.cpp b/src/basic-objects/test/currencycode_test.cpp similarity index 81% rename from src/objects/test/currencycode_test.cpp rename to src/basic-objects/test/currencycode_test.cpp index 4034cee1..a438ae8f 100644 --- a/src/objects/test/currencycode_test.cpp +++ b/src/basic-objects/test/currencycode_test.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "cct_invalid_argument_exception.hpp" #include "cct_string.hpp" @@ -122,6 +123,37 @@ TEST(CurrencyCodeTest, Size) { EXPECT_EQ(10U, CurrencyCode("Magic4Life").size()); } +TEST(CurrencyCodeTest, ResizeSameSize) { + CurrencyCode cur = "EUR"; + + cur.resize(3, 'A'); + + EXPECT_EQ("EUR", cur.str()); +} + +TEST(CurrencyCodeTest, ResizeBigger) { + CurrencyCode cur(1, 'D'); + EXPECT_EQ("D", cur.str()); + + cur = "DOGE"; + + cur.resize(7, 'X'); + + EXPECT_EQ("DOGEXXX", cur.str()); +} + +TEST(CurrencyCodeTest, ResizeSmaller) { + CurrencyCode cur = "MAGIC4LIFE"; + + cur.resize(2, 'J'); + + EXPECT_EQ("MA", cur.str()); + + cur.resize(0, 'J'); + + EXPECT_EQ("", cur.str()); +} + TEST(CurrencyCodeTest, Code) { CurrencyCode eur = "EUR"; CurrencyCode krw = "KRW"; @@ -194,4 +226,43 @@ TEST(CurrencyCodeTest, Iterator) { EXPECT_EQ("TEST", str); } +struct Foo { + bool operator==(const Foo &) const noexcept = default; + + CurrencyCode currencyCode; +}; + +TEST(CurrencyCodeTest, JsonSerializationValue) { + Foo foo{"DOGE"}; + + string buffer; + auto res = write(foo, buffer); + + EXPECT_FALSE(res); + + EXPECT_EQ(buffer, R"({"currencyCode":"DOGE"})"); +} + +using CurrencyCodeMap = std::map; + +TEST(CurrencyCodeTest, JsonSerializationKey) { + CurrencyCodeMap map{{"DOGE", true}, {"BTC", false}}; + + string buffer; + auto res = write(map, buffer); + + EXPECT_FALSE(res); + + EXPECT_EQ(buffer, R"({"BTC":false,"DOGE":true})"); +} + +TEST(CurrencyCodeTest, JsonDeserialization) { + Foo foo; + auto ec = read(foo, R"({"currencyCode":"DOGE"})"); + + ASSERT_FALSE(ec); + + EXPECT_EQ(foo, Foo{"DOGE"}); +} + } // namespace cct \ No newline at end of file diff --git a/src/objects/test/market_test.cpp b/src/basic-objects/test/market_test.cpp similarity index 85% rename from src/objects/test/market_test.cpp rename to src/basic-objects/test/market_test.cpp index 2da134fa..36322450 100644 --- a/src/objects/test/market_test.cpp +++ b/src/basic-objects/test/market_test.cpp @@ -54,4 +54,15 @@ TEST(MarketTest, StringRepresentationFiatConversionMarket) { EXPECT_EQ(market.assetsPairStrUpper('('), "*USDT(EUR"); EXPECT_EQ(market.assetsPairStrLower(')'), "*usdt)eur"); } + +TEST(MarketTest, StrLen) { + Market market("shib", "btc"); + + EXPECT_EQ(market.strLen(), 8); + EXPECT_EQ(market.strLen(false), 7); + EXPECT_EQ(market.strLen(true), 8); + + market = Market("1INCH", "EUR", Market::Type::kFiatConversionMarket); + EXPECT_EQ(market.strLen(), 10); +} } // namespace cct diff --git a/src/objects/test/monetaryamount_test.cpp b/src/basic-objects/test/monetaryamount_test.cpp similarity index 93% rename from src/objects/test/monetaryamount_test.cpp rename to src/basic-objects/test/monetaryamount_test.cpp index df99ebc5..2e4020d4 100644 --- a/src/objects/test/monetaryamount_test.cpp +++ b/src/basic-objects/test/monetaryamount_test.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "cct_exception.hpp" @@ -272,6 +273,29 @@ TEST(MonetaryAmountTest, StringConstructor) { EXPECT_NO_THROW(MonetaryAmount("usdt", MonetaryAmount::ParsingMode::kAmountOptional)); } +TEST(MonetaryAmountTest, SizeAsStrWithSpaces) { + EXPECT_EQ(MonetaryAmount("804.62EUR").strLen(), 10); + EXPECT_EQ(MonetaryAmount("-210.50 CAKE").strLen(), 11); + EXPECT_EQ(MonetaryAmount("-210.501 BTC").strLen(), 12); + EXPECT_EQ(MonetaryAmount("05AUD").strLen(), 5); + EXPECT_EQ(MonetaryAmount("746REPV2").strLen(), 9); + EXPECT_EQ(MonetaryAmount("0").strLen(), 1); + EXPECT_EQ(MonetaryAmount("35").strLen(), 2); + EXPECT_EQ(MonetaryAmount("-42").strLen(), 3); + EXPECT_EQ(MonetaryAmount("-42.7009").strLen(), 8); +} + +TEST(MonetaryAmountTest, SizeAsStrWithoutSpaces) { + EXPECT_EQ(MonetaryAmount("804.62EUR").strLen(MonetaryAmount::WithSpaces::kNo), 9); + EXPECT_EQ(MonetaryAmount("-210.50 CAKE").strLen(MonetaryAmount::WithSpaces::kNo), 10); + EXPECT_EQ(MonetaryAmount("05AUD").strLen(MonetaryAmount::WithSpaces::kNo), 4); + EXPECT_EQ(MonetaryAmount("746REPV2").strLen(MonetaryAmount::WithSpaces::kNo), 8); + EXPECT_EQ(MonetaryAmount("0").strLen(MonetaryAmount::WithSpaces::kNo), 1); + EXPECT_EQ(MonetaryAmount("35").strLen(MonetaryAmount::WithSpaces::kNo), 2); + EXPECT_EQ(MonetaryAmount("-42").strLen(MonetaryAmount::WithSpaces::kNo), 3); + EXPECT_EQ(MonetaryAmount("-42.7009").strLen(MonetaryAmount::WithSpaces::kNo), 8); +} + TEST(MonetaryAmountTest, StringConstructorAmbiguity) { EXPECT_EQ(MonetaryAmount("804.621INCH"), MonetaryAmount("804.621", "INCH")); EXPECT_EQ(MonetaryAmount("804.62 1INCH"), MonetaryAmount("804.62", "1INCH")); @@ -772,4 +796,42 @@ TEST(MonetaryAmountTest, CurrentMaxNbDecimals) { EXPECT_EQ(ma3.amount(ma3.currentMaxNbDecimals()), 389087900000000000L); } +struct Foo { + bool operator==(const Foo &) const noexcept = default; + + MonetaryAmount amount; +}; + +TEST(MonetaryAmountTest, JsonSerialization) { + Foo foo{MonetaryAmount("15.5DOGE")}; + + string buffer; + auto res = write_json(foo, buffer); + + EXPECT_FALSE(res); + + EXPECT_EQ(buffer, R"({"amount":"15.5 DOGE"})"); +} + +TEST(MonetaryAmountTest, JsonDeserializationValue) { + Foo foo; + auto ec = read(foo, R"({"amount":"15.5 DOGE"})"); + + ASSERT_FALSE(ec); + + EXPECT_EQ(foo, Foo{MonetaryAmount("15.5 DOGE")}); +} + +using MonetaryAmountMap = std::map; + +TEST(MonetaryAmountTest, JsonSerializationKey) { + MonetaryAmountMap map{{MonetaryAmount("15DOGE"), true}, {MonetaryAmount("-0.5605 DOGE"), false}}; + + string buffer; + auto res = write(map, buffer); + + EXPECT_FALSE(res); + + EXPECT_EQ(buffer, R"({"-0.5605 DOGE":false,"15 DOGE":true})"); +} } // namespace cct \ No newline at end of file diff --git a/src/http-request/src/curlhandle.cpp b/src/http-request/src/curlhandle.cpp index b94a6a56..39702af7 100644 --- a/src/http-request/src/curlhandle.cpp +++ b/src/http-request/src/curlhandle.cpp @@ -197,7 +197,7 @@ std::string_view CurlHandle::query(std::string_view endpoint, const CurlOptions if (appendParametersInQueryStr) { modifiedUrlOutIt = std::ranges::copy(postDataStr, modifiedUrlOutIt + 1).out; } else if (opts.isPostDataInJsonFormat() && !postData.empty()) { - jsonStr = postData.toJson().dump(); + jsonStr = postData.toJsonStr(); optsStr = jsonStr; } else { optsStr = postData.str(); diff --git a/src/objects/CMakeLists.txt b/src/objects/CMakeLists.txt index a81dad47..ea3b5500 100644 --- a/src/objects/CMakeLists.txt +++ b/src/objects/CMakeLists.txt @@ -3,8 +3,10 @@ aux_source_directory(src OBJECTS_SRC) add_coincenter_library(objects STATIC ${OBJECTS_SRC}) -target_link_libraries(coincenter_objects PUBLIC coincenter_monitoring) target_link_libraries(coincenter_objects PUBLIC coincenter_tech) +target_link_libraries(coincenter_objects PUBLIC coincenter_basic-objects) +target_link_libraries(coincenter_objects PUBLIC coincenter_schema) +target_link_libraries(coincenter_objects PUBLIC coincenter_monitoring) target_link_libraries(coincenter_objects PUBLIC coincenter_http-request) add_unit_test( @@ -23,15 +25,6 @@ add_unit_test( coincenter_objects ) -add_unit_test( - currencycode_test - test/currencycode_test.cpp - LIBRARIES - coincenter_tech - DEFINITIONS - CCT_DISABLE_SPDLOG -) - add_unit_test( exchangeconfig_test test/exchangeconfig_test.cpp @@ -54,15 +47,6 @@ add_unit_test( coincenter_objects ) -add_unit_test( - market_test - test/market_test.cpp - LIBRARIES - coincenter_objects - DEFINITIONS - CCT_DISABLE_SPDLOG -) - add_unit_test( marketorderbook_test test/marketorderbook_test.cpp @@ -72,15 +56,6 @@ add_unit_test( CCT_DISABLE_SPDLOG ) -add_unit_test( - monetaryamount_test - test/monetaryamount_test.cpp - LIBRARIES - coincenter_objects - DEFINITIONS - CCT_DISABLE_SPDLOG -) - add_unit_test( parseloglevel_test test/parseloglevel_test.cpp diff --git a/src/objects/include/market.hpp b/src/objects/include/market.hpp deleted file mode 100644 index c59d55a4..00000000 --- a/src/objects/include/market.hpp +++ /dev/null @@ -1,106 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "cct_format.hpp" -#include "cct_string.hpp" -#include "currencycode.hpp" -#include "monetaryamount.hpp" - -namespace cct { -/// Represents a tradable market from a currency pair. -/// Could be a fiat / coin or a coin / coin couple (fiat / fiat couple is possible but probably not relevant). -/// Important note: BTC/ETH != ETH/BTC. Use reverse() to reverse it. -class Market { - public: - enum class Type : int8_t { kRegularExchangeMarket, kFiatConversionMarket }; - - constexpr Market() noexcept(std::is_nothrow_default_constructible_v) = default; - - constexpr Market(CurrencyCode first, CurrencyCode second, Type type = Type::kRegularExchangeMarket) - : _assets({first, second}) { - setType(type); - } - - /// Create a Market from its string representation. - /// The two currency codes must be separated by given char separator. - explicit Market(std::string_view marketStrRep, char currencyCodeSep = '-', Type type = Type::kRegularExchangeMarket); - - bool isDefined() const { return base().isDefined() && quote().isDefined(); } - - bool isNeutral() const { return base().isNeutral() && quote().isNeutral(); } - - /// Computes the reverse market. - /// Example: return XRP/BTC for a market BTC/XRP - [[nodiscard]] Market reverse() const { return {_assets[1], _assets[0]}; } - - /// Get the base CurrencyCode of this Market. - CurrencyCode base() const { return _assets[0]; } - - /// Get the quote CurrencyCode of this Market. - CurrencyCode quote() const { return _assets[1]; } - - /// Given 'c' a currency traded in this Market, return the other currency it is paired with. - /// If 'c' is not traded by this market, return the second currency. - [[nodiscard]] CurrencyCode opposite(CurrencyCode cur) const { return _assets[1] == cur ? _assets[0] : _assets[1]; } - - /// Tells whether this market trades given monetary amount based on its currency. - bool canTrade(MonetaryAmount ma) const { return canTrade(ma.currencyCode()); } - - /// Tells whether this market trades given currency code. - bool canTrade(CurrencyCode cur) const { return std::ranges::find(_assets, cur) != _assets.end(); } - - constexpr auto operator<=>(const Market&) const noexcept = default; - - string str() const { return assetsPairStrUpper('-'); } - - Type type() const noexcept { return static_cast(_assets[0].getAdditionalBits()); } - - friend std::ostream& operator<<(std::ostream& os, const Market& mk); - - /// Returns a string representing this Market in lower case - string assetsPairStrLower(char sep = 0) const { return assetsPairStr(sep, true); } - - /// Returns a string representing this Market in upper case - string assetsPairStrUpper(char sep = 0) const { return assetsPairStr(sep, false); } - - private: - string assetsPairStr(char sep, bool lowerCase) const; - - constexpr void setType(Type type) { _assets[0].uncheckedSetAdditionalBits(static_cast(type)); } - - std::array _assets; -}; - -} // namespace cct - -#ifndef CCT_DISABLE_SPDLOG -template <> -struct fmt::formatter { - constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) { - auto it = ctx.begin(); - const auto end = ctx.end(); - if (it != end && *it != '}') { - throw format_error("invalid format"); - } - return it; - } - - template - auto format(const cct::Market& mk, FormatContext& ctx) const -> decltype(ctx.out()) { - return fmt::format_to(ctx.out(), "{}-{}", mk.base(), mk.quote()); - } -}; -#endif - -namespace std { -template <> -struct hash { - auto operator()(const cct::Market& mk) const { - return cct::HashCombine(hash()(mk.base()), hash()(mk.quote())); - } -}; -} // namespace std diff --git a/src/objects/src/wallet.cpp b/src/objects/src/wallet.cpp index 93e2436f..af624b27 100644 --- a/src/objects/src/wallet.cpp +++ b/src/objects/src/wallet.cpp @@ -8,20 +8,14 @@ #include "accountowner.hpp" #include "cct_const.hpp" #include "cct_exception.hpp" -#include "cct_json.hpp" #include "cct_log.hpp" #include "cct_string.hpp" #include "currencycode.hpp" +#include "deposit-addresses.hpp" #include "exchangename.hpp" -#include "file.hpp" namespace cct { -namespace { -File GetDepositAddressesFile(std::string_view dataDir) { - return {dataDir, File::Type::kSecret, kDepositAddressesFileName, File::IfError::kNoThrow}; -} -} // namespace /// Test existence of deposit address (and optional tag) in the trusted deposit addresses file. bool Wallet::ValidateWallet(WalletCheck walletCheck, const ExchangeName &exchangeName, CurrencyCode currency, std::string_view expectedAddress, std::string_view expectedTag) { @@ -29,15 +23,15 @@ bool Wallet::ValidateWallet(WalletCheck walletCheck, const ExchangeName &exchang log::debug("No wallet validation from file, consider OK"); return true; } - File depositAddressesFile = GetDepositAddressesFile(walletCheck.dataDir()); - json data = depositAddressesFile.readAllJson(); - if (!data.contains(exchangeName.name())) { + DepositAddresses depositAddresses = ReadDepositAddresses(walletCheck.dataDir()); + auto exchangeNameIt = depositAddresses.find(exchangeName.name()); + if (exchangeNameIt == depositAddresses.end()) { log::warn("No deposit addresses found in {} for {}", kDepositAddressesFileName, exchangeName); return false; } - const json &exchangeWallets = data[string(exchangeName.name())]; + const ExchangeDepositAddresses &exchangeDepositAddresses = exchangeNameIt->second; bool uniqueKeyName = true; - for (const auto &[privateExchangeKeyName, wallets] : exchangeWallets.items()) { + for (const auto &[privateExchangeKeyName, accountDepositAddresses] : exchangeDepositAddresses) { if (exchangeName.keyName().empty()) { if (!uniqueKeyName) { log::error("Several key names found for exchange {:n}. Specify a key name to remove ambiguity", exchangeName); @@ -48,11 +42,9 @@ bool Wallet::ValidateWallet(WalletCheck walletCheck, const ExchangeName &exchang } else if (exchangeName.keyName() != privateExchangeKeyName) { continue; } - for (const auto &[currencyCodeStr, value] : wallets.items()) { - CurrencyCode currencyCode(currencyCodeStr); + for (const auto &[currencyCode, addressAndTag] : accountDepositAddresses) { if (currencyCode == currency) { - std::string_view addressAndTag = value.get(); - std::size_t tagPos = addressAndTag.find(','); + auto tagPos = addressAndTag.find(','); std::string_view address(addressAndTag.begin(), addressAndTag.begin() + std::min(tagPos, addressAndTag.size())); if (expectedAddress != address) { return false; diff --git a/src/schema/CMakeLists.txt b/src/schema/CMakeLists.txt new file mode 100644 index 00000000..d4772378 --- /dev/null +++ b/src/schema/CMakeLists.txt @@ -0,0 +1,12 @@ +aux_source_directory(src SCHEMA_SRC) + +add_coincenter_library(schema STATIC ${SCHEMA_SRC}) + +target_link_libraries(coincenter_schema PUBLIC coincenter_basic-objects) + +add_unit_test( + deposit-addresses_test + test/deposit-addresses_test.cpp + LIBRARIES + coincenter_schema +) \ No newline at end of file diff --git a/src/schema/include/deposit-addresses.hpp b/src/schema/include/deposit-addresses.hpp new file mode 100644 index 00000000..9cb5ac43 --- /dev/null +++ b/src/schema/include/deposit-addresses.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +#include "cct_string.hpp" +#include "currencycode.hpp" + +namespace cct { + +using AccountDepositAddresses = std::map>; + +using ExchangeDepositAddresses = std::map>; + +using DepositAddresses = std::map>; + +DepositAddresses ReadDepositAddresses(std::string_view dataDir); + +} // namespace cct diff --git a/src/schema/include/read-json.hpp b/src/schema/include/read-json.hpp new file mode 100644 index 00000000..d6778037 --- /dev/null +++ b/src/schema/include/read-json.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "cct_exception.hpp" +#include "cct_json-serialization.hpp" +#include "reader.hpp" + +namespace cct { + +namespace { +constexpr auto JsonOptions = opts{.raw_string = true}; +} + +template +T ReadJsonOrThrow(std::string_view strContent) { + T outObject; + + auto ec = read(outObject, strContent); + + if (ec) { + throw exception("Error while reading json content: {}", format_error(ec, strContent)); + } + + return outObject; +} + +template +T ReadJsonOrThrow(const Reader &reader) { + return ReadJsonOrThrow(reader.readAll()); +} + +} // namespace cct \ No newline at end of file diff --git a/src/schema/src/deposit-addresses.cpp b/src/schema/src/deposit-addresses.cpp new file mode 100644 index 00000000..d2139cfb --- /dev/null +++ b/src/schema/src/deposit-addresses.cpp @@ -0,0 +1,19 @@ +#include "deposit-addresses.hpp" + +#include "cct_const.hpp" +#include "file.hpp" +#include "read-json.hpp" + +namespace cct { + +namespace { +File GetDepositAddressesFile(std::string_view dataDir) { + return {dataDir, File::Type::kSecret, kDepositAddressesFileName, File::IfError::kNoThrow}; +} +} // namespace + +DepositAddresses ReadDepositAddresses(std::string_view dataDir) { + return ReadJsonOrThrow(GetDepositAddressesFile(dataDir)); +} + +} // namespace cct \ No newline at end of file diff --git a/src/schema/test/deposit-addresses_test.cpp b/src/schema/test/deposit-addresses_test.cpp new file mode 100644 index 00000000..0aaeb6c3 --- /dev/null +++ b/src/schema/test/deposit-addresses_test.cpp @@ -0,0 +1,50 @@ +#include "deposit-addresses.hpp" + +#include + +#include "read-json.hpp" +#include "reader.hpp" + +namespace cct::schema { +TEST(DepositAddressesTest, NominalCase) { + class NominalCase : public Reader { + [[nodiscard]] string readAll() const override { + return R"( +{ + "binance": { + "user1": { + "EUR": "0x1234567890abcde1", + "DOGE": "D123456789" + } + }, + "kraken": { + "user1": { + "EUR": "0x1234567890abcdefg2", + "DOGE": "D123456789" + }, + "user2": { + "EUR": "0x1234567890abcdef3", + "ETH": "0xETHaddress" + } + } +} +)"; + } + }; + + DepositAddresses depositAddresses = ReadJsonOrThrow(NominalCase{}); + + EXPECT_EQ(depositAddresses.size(), 2); + EXPECT_EQ(depositAddresses.at("binance").size(), 1); + EXPECT_EQ(depositAddresses.at("kraken").size(), 2); + EXPECT_EQ(depositAddresses.at("binance").at("user1").size(), 2); + EXPECT_EQ(depositAddresses.at("kraken").at("user1").size(), 2); + EXPECT_EQ(depositAddresses.at("kraken").at("user2").size(), 2); + EXPECT_EQ(depositAddresses.at("binance").at("user1").at("EUR"), "0x1234567890abcde1"); + EXPECT_EQ(depositAddresses.at("binance").at("user1").at("DOGE"), "D123456789"); + EXPECT_EQ(depositAddresses.at("kraken").at("user1").at("EUR"), "0x1234567890abcdefg2"); + EXPECT_EQ(depositAddresses.at("kraken").at("user1").at("DOGE"), "D123456789"); + EXPECT_EQ(depositAddresses.at("kraken").at("user2").at("EUR"), "0x1234567890abcdef3"); + EXPECT_EQ(depositAddresses.at("kraken").at("user2").at("ETH"), "0xETHaddress"); +} +} // namespace cct::schema \ No newline at end of file diff --git a/src/tech/CMakeLists.txt b/src/tech/CMakeLists.txt index 6a31558e..30735843 100644 --- a/src/tech/CMakeLists.txt +++ b/src/tech/CMakeLists.txt @@ -4,6 +4,8 @@ aux_source_directory(src API_TECH_SRC) add_coincenter_library(tech STATIC ${API_TECH_SRC}) target_link_libraries(coincenter_tech PUBLIC nlohmann_json::nlohmann_json) +target_link_libraries(coincenter_tech PUBLIC glaze::glaze) + if (LINK_AMC) target_link_libraries(coincenter_tech PUBLIC amc::amc) endif() diff --git a/src/tech/include/cct_json-serialization.hpp b/src/tech/include/cct_json-serialization.hpp new file mode 100644 index 00000000..725ad358 --- /dev/null +++ b/src/tech/include/cct_json-serialization.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace cct { + +using glz::format_error; +using glz::opts; +using glz::read; +using glz::read_json; +using glz::write; +using glz::write_json; + +} // namespace cct \ No newline at end of file diff --git a/src/tech/include/flatkeyvaluestring.hpp b/src/tech/include/flatkeyvaluestring.hpp index 2ba23d61..2d215f4d 100644 --- a/src/tech/include/flatkeyvaluestring.hpp +++ b/src/tech/include/flatkeyvaluestring.hpp @@ -16,10 +16,8 @@ #include #include "cct_cctype.hpp" -#include "cct_json.hpp" #include "cct_string.hpp" #include "cct_type_traits.hpp" -#include "cct_vector.hpp" #include "flat-key-value-string-iterator.hpp" #include "unreachable.hpp" #include "url-encode.hpp" @@ -187,11 +185,11 @@ class FlatKeyValueString { /// The returned string_view is guaranteed to be null-terminated. std::string_view str() const noexcept { return _data; } - /// Converts to a json document. + /// Converts to a json document string. /// Values ending with a ',' will be considered as arrays. /// In this case, sub array values are comma separated values. /// Limitation: all json values will be decoded as strings. - json toJson() const; + string toJsonStr() const; /// Returns a new FlatKeyValueString URL encoded except delimiters. FlatKeyValueString urlEncodeExceptDelimiters() const; @@ -390,24 +388,44 @@ std::string_view FlatKeyValueString::Get(std::s } template -json FlatKeyValueString::toJson() const { - json ret; +string FlatKeyValueString::toJsonStr() const { + string ret; + ret.reserve(2 * (_data.size() + 1U)); + ret.push_back('{'); + + const auto appendStr = [&ret](std::string_view str) { + ret.push_back('"'); + ret.append(str); + ret.push_back('"'); + }; + for (const auto &kv : *this) { const auto key = kv.key(); const auto val = kv.val(); + if (ret.size() != 1U) { + ret.push_back(','); + } + appendStr(key); + ret.push_back(':'); + auto valSize = val.size(); if (valSize == 0 || val.back() != kArrayElemSepChar) { - ret.emplace(key, val); + // standard field case + appendStr(val); continue; } - vector arrayValues; + // array case + ret.push_back('['); if (valSize != 1U) { // Check empty array case for (std::size_t arrayValBeg = 0;;) { std::size_t arrayValSepPos = val.find(kArrayElemSepChar, arrayValBeg); - arrayValues.emplace_back(std::string_view(val.begin() + arrayValBeg, val.begin() + arrayValSepPos)); + if (arrayValBeg != 0) { + ret.push_back(','); + } + appendStr(std::string_view(val.begin() + arrayValBeg, val.begin() + arrayValSepPos)); if (arrayValSepPos + 1U == valSize) { break; } @@ -415,8 +433,9 @@ json FlatKeyValueString::toJson() const { } } - ret.emplace(key, std::move(arrayValues)); + ret.push_back(']'); } + ret.push_back('}'); return ret; } diff --git a/src/tech/test/flatkeyvaluestring_test.cpp b/src/tech/test/flatkeyvaluestring_test.cpp index 968386ce..1a919f72 100644 --- a/src/tech/test/flatkeyvaluestring_test.cpp +++ b/src/tech/test/flatkeyvaluestring_test.cpp @@ -6,7 +6,6 @@ #include #include "cct_exception.hpp" -#include "cct_json.hpp" #include "cct_string.hpp" namespace cct { @@ -112,15 +111,15 @@ TEST(FlatKeyValueStringTest, WithNullTerminatingCharAsSeparator) { using ExoticKeyValuePair = FlatKeyValueString<'\0', ':'>; ExoticKeyValuePair kvPairs{{"abc", "354"}, {"tata", "abc"}, {"rm", "xX"}, {"huhu", "haha"}}; - EXPECT_EQ(kvPairs.str(), std::string_view("abc:354\0tata:abc\0rm:xX\0huhu:haha"sv)); + EXPECT_EQ(kvPairs.str(), "abc:354\0tata:abc\0rm:xX\0huhu:haha"sv); kvPairs.set("rm", "Yy3"); - EXPECT_EQ(kvPairs.str(), std::string_view("abc:354\0tata:abc\0rm:Yy3\0huhu:haha"sv)); + EXPECT_EQ(kvPairs.str(), "abc:354\0tata:abc\0rm:Yy3\0huhu:haha"sv); kvPairs.erase("abc"); - EXPECT_EQ(kvPairs.str(), std::string_view("tata:abc\0rm:Yy3\0huhu:haha"sv)); + EXPECT_EQ(kvPairs.str(), "tata:abc\0rm:Yy3\0huhu:haha"sv); kvPairs.erase("rm"); - EXPECT_EQ(kvPairs.str(), std::string_view("tata:abc\0huhu:haha"sv)); + EXPECT_EQ(kvPairs.str(), "tata:abc\0huhu:haha"sv); kvPairs.emplace_back("&newField", "&&newValue&&"); - EXPECT_EQ(kvPairs.str(), std::string_view("tata:abc\0huhu:haha\0&newField:&&newValue&&"sv)); + EXPECT_EQ(kvPairs.str(), "tata:abc\0huhu:haha\0&newField:&&newValue&&"sv); int kvPairPos = 0; for (const auto &kv : kvPairs) { @@ -143,7 +142,7 @@ TEST(FlatKeyValueStringTest, WithNullTerminatingCharAsSeparator) { EXPECT_EQ(kvPairPos, 3); } -TEST(FlatKeyValueStringTest, EmptyConvertToJson) { EXPECT_EQ(KvPairs().toJson(), json()); } +TEST(FlatKeyValueStringTest, EmptyConvertToJson) { EXPECT_EQ(KvPairs().toJsonStr(), "{}"); } class FlatKeyValueStringCase1 : public ::testing::Test { protected: @@ -346,37 +345,10 @@ TEST_F(FlatKeyValueStringCase1, EraseIncrementDecrement) { EXPECT_EQ(itPos, 5); } -TEST_F(FlatKeyValueStringCase1, ConvertToJson) { - json jsonData = kvPairs.toJson(); - - EXPECT_EQ(jsonData["units"].get(), "0.11176"); - EXPECT_EQ(jsonData["price"].get(), "357.78"); - EXPECT_EQ(jsonData["777"].get(), "encoredutravail?"); - EXPECT_EQ(jsonData["hola"].get(), "quetal"); - EXPECT_FALSE(jsonData["hola"].is_array()); - - auto arrayIt = jsonData.find("array1"); - EXPECT_NE(arrayIt, jsonData.end()); - EXPECT_TRUE(arrayIt->is_array()); - EXPECT_EQ(arrayIt->size(), 2U); - - EXPECT_EQ((*arrayIt)[0], "val1"); - EXPECT_EQ((*arrayIt)[1], ""); - - arrayIt = jsonData.find("array2"); - EXPECT_NE(arrayIt, jsonData.end()); - EXPECT_TRUE(arrayIt->is_array()); - EXPECT_EQ(arrayIt->size(), 4U); - - EXPECT_EQ((*arrayIt)[0], ""); - EXPECT_EQ((*arrayIt)[1], "val1"); - EXPECT_EQ((*arrayIt)[2], "val2"); - EXPECT_EQ((*arrayIt)[3], "value"); - - arrayIt = jsonData.find("emptyArray"); - EXPECT_NE(arrayIt, jsonData.end()); - EXPECT_TRUE(arrayIt->is_array()); - EXPECT_TRUE(arrayIt->empty()); +TEST_F(FlatKeyValueStringCase1, ConvertToJsonStr) { + EXPECT_EQ( + kvPairs.toJsonStr(), + R"({"units":"0.11176","price":"357.78","777":"encoredutravail?","hola":"quetal","k":"v","array1":["val1",""],"array2":["","val1","val2","value"],"emptyArray":[]})"); } TEST_F(FlatKeyValueStringCase1, AppendIntegralValues) {