Skip to content

Commit

Permalink
Increase maximum number of characters of currency code to 10. Throw i…
Browse files Browse the repository at this point in the history
…nvalid argument if MonetaryAmount is built with a CurrencyCode too long, and in CLI as well
  • Loading branch information
sjanel committed Oct 24, 2022
1 parent d6637ee commit 784127f
Show file tree
Hide file tree
Showing 15 changed files with 436 additions and 239 deletions.
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.15)

project(coincenter VERSION 3.16.1
project(coincenter VERSION 3.17.0
DESCRIPTION "A C++ library centralizing several crypto currencies exchanges REST API into a single all in one tool with a unified interface"
LANGUAGES CXX)

Expand Down
8 changes: 4 additions & 4 deletions src/api/exchanges/src/bithumbprivateapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ Orders BithumbPrivate::queryOpenedOrders(const OrdersConstraints& openedOrdersCo
if (!filterMarket.base().isNeutral()) {
orderCurrencies.push_back(filterMarket.base());
if (!filterMarket.quote().isNeutral()) {
params.append(kPaymentCurParamStr, filterMarket.quoteStr());
params.append(kPaymentCurParamStr, filterMarket.quote().str());
}
}
} else {
Expand Down Expand Up @@ -399,7 +399,7 @@ PlaceOrderInfo BithumbPrivate::placeOrder(MonetaryAmount /*from*/, MonetaryAmoun
const Market m = tradeInfo.m;

// It seems Bithumb uses "standard" currency codes, no need to translate them
CurlPostData placePostData{{kOrderCurrencyParamStr, m.baseStr()}, {kPaymentCurParamStr, m.quoteStr()}};
CurlPostData placePostData{{kOrderCurrencyParamStr, m.base().str()}, {kPaymentCurParamStr, m.quote().str()}};
const std::string_view orderType = fromCurrencyCode == m.base() ? "ask" : "bid";

string endpoint("/trade/");
Expand Down Expand Up @@ -557,8 +557,8 @@ OrderInfo BithumbPrivate::cancelOrder(const OrderRef& orderRef) {
namespace {
CurlPostData OrderInfoPostData(Market m, TradeSide side, std::string_view id) {
CurlPostData ret;
std::string_view baseStr = m.baseStr();
std::string_view quoteStr = m.quoteStr();
auto baseStr = m.base().str();
auto quoteStr = m.quote().str();
ret.reserve(kOrderCurrencyParamStr.size() + kPaymentCurParamStr.size() + kTypeParamStr.size() +
kOrderIdParamStr.size() + baseStr.size() + quoteStr.size() + id.size() + 10U);
ret.append(kOrderCurrencyParamStr, baseStr);
Expand Down
18 changes: 13 additions & 5 deletions src/api/exchanges/src/krakenpublicapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -367,12 +367,16 @@ MarketOrderBookMap KrakenPublic::AllOrderBooksFunc::operator()(int depth) {
for (Market m : markets) {
auto lb = krakenCurrencies.find(m.base());
if (lb == krakenCurrencies.end()) {
throw exception("Cannot find " + string(m.baseStr()) + " in Kraken currencies");
string msg("Cannot find ");
msg.append(m.base().str()).append(" in Kraken currencies");
throw exception(std::move(msg));
}
CurrencyExchange krakenCurrencyExchangeBase = *lb;
lb = krakenCurrencies.find(m.quote());
if (lb == krakenCurrencies.end()) {
throw exception("Cannot find " + string(m.quoteStr()) + " in Kraken currencies");
string msg("Cannot find ");
msg.append(m.quote().str()).append(" in Kraken currencies");
throw exception(std::move(msg));
}
CurrencyExchange krakenCurrencyExchangeQuote = *lb;
Market krakenMarket(krakenCurrencyExchangeBase.altCode(), krakenCurrencyExchangeQuote.altCode());
Expand Down Expand Up @@ -421,15 +425,19 @@ MarketOrderBook KrakenPublic::OrderBookFunc::operator()(Market m, int count) {
CurrencyExchangeFlatSet krakenCurrencies = _tradableCurrenciesCache.get();
auto lb = krakenCurrencies.find(m.base());
if (lb == krakenCurrencies.end()) {
throw exception("Cannot find " + string(m.baseStr()) + " in Kraken currencies");
string msg("Cannot find ");
msg.append(m.base().str()).append(" in Kraken currencies");
throw exception(std::move(msg));
}
CurrencyExchange krakenCurrencyExchangeBase = *lb;
lb = krakenCurrencies.find(m.quote());
if (lb == krakenCurrencies.end()) {
throw exception("Cannot find " + string(m.quoteStr()) + " in Kraken currencies");
string msg("Cannot find ");
msg.append(m.quote().str()).append(" in Kraken currencies");
throw exception(std::move(msg));
}
CurrencyExchange krakenCurrencyExchangeQuote = *lb;
string krakenAssetPair(krakenCurrencyExchangeBase.altStr());
string krakenAssetPair = krakenCurrencyExchangeBase.altStr();
krakenAssetPair.append(krakenCurrencyExchangeQuote.altStr());
json result = PublicQuery(_curlHandle, "/public/Depth", {{"pair", krakenAssetPair}, {"count", count}});
const json& entry = result.front();
Expand Down
2 changes: 1 addition & 1 deletion src/engine/src/exchangesorchestrator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ MarketOrderBookConversionRates ExchangesOrchestrator::getMarketOrderBooks(Market
: e->apiPublic().convert(MonetaryAmount(1, m.quote()), equiCurrencyCode);
MarketOrderBook marketOrderBook(depth ? e->queryOrderBook(m, *depth) : e->queryOrderBook(m));
if (!optConversionRate && !equiCurrencyCode.isNeutral()) {
log::warn("Unable to convert {} into {} on {}", marketOrderBook.market().quoteStr(), equiCurrencyCode.str(),
log::warn("Unable to convert {} into {} on {}", marketOrderBook.market().quote().str(), equiCurrencyCode.str(),
e->name());
}
return std::make_tuple(e->name(), std::move(marketOrderBook), optConversionRate);
Expand Down
6 changes: 3 additions & 3 deletions src/engine/src/queryresultprinter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -587,12 +587,12 @@ void QueryResultPrinter::printLastTrades(Market m, int nbLastTrades,
switch (_apiOutputType) {
case ApiOutputType::kFormattedTable: {
for (const auto &[exchangePtr, lastTrades] : lastTradesPerExchange) {
string buyTitle(m.baseStr());
string buyTitle = m.base().str();
string sellTitle = buyTitle;
buyTitle.append(" buys");
string sellTitle(m.baseStr());
sellTitle.append(" sells");
string priceTitle("Price in ");
priceTitle.append(m.quoteStr());
priceTitle.append(m.quote().str());

string title(exchangePtr->name());
title.append(" trades - UTC");
Expand Down
7 changes: 3 additions & 4 deletions src/engine/src/stringoptionparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ auto GetNextPercentageAmount(std::string_view opt, std::string_view sepWithPerce
}
return std::make_pair(std::move(startAmount), isPercentage);
}

} // namespace

ExchangeNames StringOptionParser::getExchanges() const { return GetExchanges(_opt); }
Expand Down Expand Up @@ -131,11 +130,11 @@ StringOptionParser::CurrenciesPrivateExchanges StringOptionParser::getCurrencies
std::string_view token1 = GetNextStr(_opt, ',', pos);
if (!IsExchangeName(token1)) {
startExchangesPos = pos;
fromTradeCurrency = token1;
fromTradeCurrency = CurrencyCode(token1);
std::string_view token2 = GetNextStr(_opt, ',', pos);
if (!IsExchangeName(token2)) {
startExchangesPos = pos;
toTradeCurrency = token2;
toTradeCurrency = CurrencyCode(token2);
}
}
} else {
Expand Down Expand Up @@ -238,4 +237,4 @@ vector<std::string_view> StringOptionParser::getCSVValues() const {
return ret;
}

} // namespace cct
} // namespace cct
4 changes: 4 additions & 0 deletions src/engine/test/stringoptionparser_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ TEST(StringOptionParserTest, GetCurrencyPrivateExchanges) {
.getCurrencyPrivateExchanges(StringOptionParser::CurrencyIs::kMandatory),
std::make_pair(CurrencyCode("KRW"), ExchangeNames({ExchangeName("bithumb"), ExchangeName("binance", "user1")})));

EXPECT_THROW(StringOptionParser("toolongcurrency,Bithumb,binance_user1").getCurrencyPrivateExchanges(optionalCur),
invalid_argument);
EXPECT_THROW(StringOptionParser("binance_user1,bithumb")
.getCurrencyPrivateExchanges(StringOptionParser::CurrencyIs::kMandatory),
invalid_argument);
Expand All @@ -42,6 +44,8 @@ TEST(StringOptionParserTest, GetMarketExchanges) {
EXPECT_EQ(StringOptionParser("dash-krw,bithumb,upbit").getMarketExchanges(),
StringOptionParser::MarketExchanges(Market("DASH", "KRW"),
ExchangeNames({ExchangeName("bithumb"), ExchangeName("upbit")})));

EXPECT_THROW(StringOptionParser("dash-toolongcurrency,bithumb,upbit").getMarketExchanges(), invalid_argument);
}

TEST(StringOptionParserTest, GetMonetaryAmountPrivateExchanges) {
Expand Down
171 changes: 130 additions & 41 deletions src/objects/include/currencycode.hpp
Original file line number Diff line number Diff line change
@@ -1,83 +1,172 @@
#pragma once

#include <algorithm>
#include <array>
#include <compare>
#include <cstdint>
#include <ostream>
#include <string_view>
#include <type_traits>

#include "cct_hash.hpp"
#include "cct_log.hpp"
#include "toupperlower.hpp"
#include "cct_invalid_argument_exception.hpp"

namespace cct {

/// Lightweight object representing a currency code with its acronym. Can be used as a key.
/// Can be used to represent a fiat currency or a coin (for the latter, acronym is expected to be 7 chars long maximum)
/// Can be used to represent a fiat currency or a coin (for the latter, acronym is expected to be 10 chars long maximum)
class CurrencyCode {
public:
static constexpr int kAcronymMaxLen = 7;

using AcronymType = std::array<char, kAcronymMaxLen>; // warning: not null terminated
static constexpr int kAcronymMaxLen = 10;

/// Constructs a neutral currency code.
constexpr CurrencyCode() noexcept : _data() {}
constexpr CurrencyCode() noexcept : _data(0) {}

/// Constructs a currency code from a static char array.
/// Note: spaces are not skipped. If any, there will be captured as part of the code, which is probably unexpected.
/// Constructs a currency code from a char array.
template <unsigned N, std::enable_if_t<N <= kAcronymMaxLen + 1, bool> = true>
constexpr CurrencyCode(const char (&acronym)[N]) noexcept {
set(acronym);
}
constexpr CurrencyCode(const char (&acronym)[N]) : _data(ComputeData(acronym)) {}

/// Constructs a currency code from given string.
/// Note: spaces are not skipped. If any, there will be captured as part of the code, which is probably unexpected.
/// If number of chars in 'acronym' is higher than 'kAcronymMaxLen', exception will be raised.
/// Note: spaces are not skipped. If any, they will be captured as part of the code, which is probably unexpected.
constexpr CurrencyCode(std::string_view acronym) {
if (_data.size() < acronym.size()) {
if (!std::is_constant_evaluated()) {
log::debug("Acronym {} is too long, truncating to {} chars", acronym, kAcronymMaxLen);
}
acronym.remove_suffix(acronym.size() - _data.size());
if (acronym.length() > kAcronymMaxLen) {
throw invalid_argument("Acronym is too long to fit in a CurrencyCode");
}
set(acronym);
_data = ComputeData(acronym);
}

constexpr uint64_t size() const { return std::ranges::find(_data, '\0') - _data.begin(); }
constexpr uint64_t size() const {
uint64_t s;
for (s = 0; static_cast<int>(s) < kAcronymMaxLen && (_data & (kFirstCharMask >> (kNbBitsChar * s))); ++s)
;
return s;
}
constexpr uint64_t length() const { return size(); }

/// Get a string view of this CurrencyCode, trimmed.
constexpr std::string_view str() const { return std::string_view(_data.begin(), std::ranges::find(_data, '\0')); }
/// Get a string of this CurrencyCode, trimmed.
string str() const {
string s;
appendStr(s);
return s;
}

/// Returns a 64 bits code
constexpr uint64_t code() const noexcept {
uint64_t ret = _data[6];
ret |= static_cast<uint64_t>(_data[5]) << 8;
ret |= static_cast<uint64_t>(_data[4]) << 16;
ret |= static_cast<uint64_t>(_data[3]) << 24;
ret |= static_cast<uint64_t>(_data[2]) << 32;
ret |= static_cast<uint64_t>(_data[1]) << 40;
ret |= static_cast<uint64_t>(_data[0]) << 48;
return ret;
}
constexpr uint64_t code() const noexcept { return _data; }

constexpr bool isNeutral() const noexcept { return !(_data & kFirstCharMask); }

constexpr bool isNeutral() const noexcept { return _data.front() == '\0'; }
constexpr char operator[](uint32_t pos) const {
return static_cast<char>(
(_data >> (kNbBitsNbDecimals + kNbBitsChar * (kAcronymMaxLen - static_cast<int>(pos) - 1))) &
((1ULL << kNbBitsChar) - 1ULL)) +
kFirstAuthorizedLetter - 1;
}

constexpr auto operator<=>(const CurrencyCode &) const = default;

constexpr bool operator==(const CurrencyCode &) const = default;

friend std::ostream &operator<<(std::ostream &os, const CurrencyCode &c) {
os << c.str();
friend std::ostream &operator<<(std::ostream &os, const CurrencyCode &cur) {
for (uint32_t charPos = 0; charPos < kAcronymMaxLen; ++charPos) {
char c = cur[charPos];
if (c == kFirstAuthorizedLetter - 1) {
break;
}
os << c;
}
return os;
}

private:
AcronymType _data;
friend class MonetaryAmount;

constexpr inline void set(std::string_view acronym) {
// Fill extra chars to 0 is important as we always read them for code generation
std::fill(std::transform(acronym.begin(), acronym.end(), _data.begin(), toupper), _data.end(), '\0');
static constexpr uint64_t kNbBitsChar = 6;
static constexpr uint64_t kNbBitsNbDecimals = 4;

static constexpr uint64_t kNbDecimals4Mask = (1ULL << kNbBitsNbDecimals) - 1ULL;

static constexpr uint64_t kFirstCharMask =
~((1ULL << (kNbBitsNbDecimals + (kAcronymMaxLen - 1) * kNbBitsChar)) - 1ULL);

static constexpr uint64_t kBeforeLastCharMask = kFirstCharMask >> (kNbBitsChar * (kAcronymMaxLen - 2));

static constexpr int64_t kMaxNbDecimalsLongCurrencyCode = 15; // 2^4 bits

static constexpr char kFirstAuthorizedLetter = 33; // '!'
static constexpr char kLastAuthorizedLetter = 95; // '_'

// 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)
uint64_t _data;

explicit constexpr CurrencyCode(uint64_t data) : _data(data) {}

constexpr bool isLongCurrencyCode() const { return _data & kBeforeLastCharMask; }

constexpr void setNbDecimals(int8_t nbDecimals) {
if (isLongCurrencyCode()) {
if (!std::is_constant_evaluated() && nbDecimals > kMaxNbDecimalsLongCurrencyCode) {
throw invalid_argument("Too many decimals for long currency code");
}
// For currency codes whose length is > 8, only 15 digits are supported
_data = static_cast<uint64_t>(nbDecimals) + (_data & (~kNbDecimals4Mask));
} else {
// max 64 decimals for currency codes whose length is maximum 8 (most cases)
_data = (static_cast<uint64_t>(nbDecimals) << kNbBitsNbDecimals) +
(_data & ~((1ULL << (kNbBitsChar + kNbBitsNbDecimals)) - 1ULL));
}
}

constexpr int8_t nbDecimals() const {
if (isLongCurrencyCode()) {
// For currency codes whose length is > 8, only 15 digits are supported
return static_cast<int8_t>(_data & kNbDecimals4Mask);
}
// max 64 decimals for currency codes whose length is maximum 8 (most cases)
return static_cast<int8_t>((_data >> kNbBitsNbDecimals) & ((1ULL << kNbBitsChar) - 1ULL));
}

constexpr CurrencyCode toNeutral() const {
// keep number of decimals
return CurrencyCode(_data &
(isLongCurrencyCode() ? kNbDecimals4Mask : (1ULL << (kNbBitsChar + kNbBitsNbDecimals)) - 1ULL));
}

constexpr CurrencyCode withNoDecimalsPart() const {
// Keep currency, not decimals
return CurrencyCode(
_data & ~(isLongCurrencyCode() ? kNbDecimals4Mask : (1ULL << (kNbBitsChar + kNbBitsNbDecimals)) - 1ULL));
}

void appendStr(string &s) const {
for (uint32_t charPos = 0; charPos < kAcronymMaxLen; ++charPos) {
char c = (*this)[charPos];
if (c == kFirstAuthorizedLetter - 1) {
break;
}
s.push_back(c);
}
}

static constexpr uint64_t ComputeData(std::string_view acronym) {
uint64_t ret = 0;
int charPos = 0;
for (char c : acronym) {
if (c >= 'a') {
if (c > 'z') {
throw invalid_argument("Unexpected char in acronym");
}
c -= 'a' - 'A';
}
if (c < kFirstAuthorizedLetter || c > kLastAuthorizedLetter) {
throw invalid_argument("Unexpected char in acronym");
}

ret |= static_cast<uint64_t>(c - kFirstAuthorizedLetter + 1)
<< (kNbBitsNbDecimals + kNbBitsChar * (kAcronymMaxLen - charPos - 1));

++charPos;
}
return ret;
}
};

Expand Down
6 changes: 3 additions & 3 deletions src/objects/include/currencyexchange.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ class CurrencyExchange {
CurrencyExchange(CurrencyCode standardCode, CurrencyCode exchangeCode, CurrencyCode altCode, Deposit deposit,
Withdraw withdraw, Type type);

std::string_view standardStr() const { return _standardCode.str(); }
std::string_view exchangeStr() const { return _exchangeCode.str(); }
std::string_view altStr() const { return _altCode.str(); }
string standardStr() const { return _standardCode.str(); }
string exchangeStr() const { return _exchangeCode.str(); }
string altStr() const { return _altCode.str(); }

string str() const;

Expand Down
Loading

0 comments on commit 784127f

Please sign in to comment.