diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 56fff5d5..eb3ae030 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,7 +57,7 @@ jobs: # } steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Download Ninja and CMake id: cmake_and_ninja diff --git a/README.md b/README.md index e86f9e04..7e1d03f6 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ - [Override exchange urls](#override-exchange-urls) - [Connect to a proxy](#connect-to-a-proxy) - [Reduce build time](#reduce-build-time) + - [Exchange Specific Notes](#exchange-specific-notes) + - [Hyperliquid](#hyperliquid) - [Performance Tuning](#performance-tuning) - [Known Issues and Workarounds](#known-issues-and-workarounds) @@ -57,8 +59,8 @@ * Code closely follows Bloomberg's API: https://www.bloomberg.com/professional/support/api-library/. * It is ultra fast thanks to very careful optimizations. * Supported exchanges: - * Market Data: ascendex, [binance](https://accounts.maxweb.academy/register?ref=1116718520), [binance-usds-futures](https://accounts.maxweb.academy/register?ref=1116718520), [binance-coin-futures](https://accounts.maxweb.academy/register?ref=1116718520), bitfinex, bitget, bitget-futures, bitmart, bitmex, bitstamp, [bybit](https://www.bybit.com/invite?ref=XNYP2K), coinbase, [cryptocom](https://crypto.com/exch/tqj4b8x48w), deribit, erisx (Cboe Digital), [gateio](https://www.gate.com/signup/VLUQXVFWAW?ref_type=103), [gateio-perpetual-futures](https://www.gate.com/signup/VLUQXVFWAW?ref_type=103), gemini, [huobi](https://www.htx.com/invite/en-us/1f?invite_code=rmw7d223), [huobi-usdt-swap](https://www.htx.com/invite/en-us/1f?invite_code=rmw7d223), [huobi-coin-swap](https://www.htx.com/invite/en-us/1f?invite_code=rmw7d223), kraken, kraken-futures, kucoin, kucoin-futures, mexc, mexc-futures, [okx](https://www.okx.com/join/47636709), whitebit. - * Execution Management: ascendex, [binance](https://accounts.maxweb.academy/register?ref=1116718520), [binance-usds-futures](https://accounts.maxweb.academy/register?ref=1116718520), [binance-coin-futures](https://accounts.maxweb.academy/register?ref=1116718520), bitfinex, bitget, bitget-futures, bitmart, bitmex, bitstamp, [bybit](https://www.bybit.com/invite?ref=XNYP2K), coinbase, [cryptocom](https://crypto.com/exch/tqj4b8x48w), deribit, erisx (Cboe Digital), [gateio](https://www.gate.com/signup/VLUQXVFWAW?ref_type=103), [gateio-perpetual-futures](https://www.gate.com/signup/VLUQXVFWAW?ref_type=103), gemini, [huobi](https://www.htx.com/invite/en-us/1f?invite_code=rmw7d223), [huobi-usdt-swap](https://www.htx.com/invite/en-us/1f?invite_code=rmw7d223), [huobi-coin-swap](https://www.htx.com/invite/en-us/1f?invite_code=rmw7d223), kraken, kraken-futures, kucoin, kucoin-futures, mexc, [okx](https://www.okx.com/join/47636709). + * Market Data: ascendex, [binance](https://accounts.maxweb.academy/register?ref=1116718520), [binance-usds-futures](https://accounts.maxweb.academy/register?ref=1116718520), [binance-coin-futures](https://accounts.maxweb.academy/register?ref=1116718520), bitfinex, bitget, bitget-futures, bitmart, bitmex, bitstamp, [bybit](https://www.bybit.com/invite?ref=XNYP2K), coinbase, [cryptocom](https://crypto.com/exch/tqj4b8x48w), deribit, erisx (Cboe Digital), [gateio](https://www.gate.com/signup/VLUQXVFWAW?ref_type=103), [gateio-perpetual-futures](https://www.gate.com/signup/VLUQXVFWAW?ref_type=103), gemini, [huobi](https://www.htx.com/invite/en-us/1f?invite_code=rmw7d223), [huobi-usdt-swap](https://www.htx.com/invite/en-us/1f?invite_code=rmw7d223), [huobi-coin-swap](https://www.htx.com/invite/en-us/1f?invite_code=rmw7d223), [hyperliquid](https://app.hyperliquid.xyz/), kraken, kraken-futures, kucoin, kucoin-futures, mexc, mexc-futures, [okx](https://www.okx.com/join/47636709), whitebit. + * Execution Management: ascendex, [binance](https://accounts.maxweb.academy/register?ref=1116718520), [binance-usds-futures](https://accounts.maxweb.academy/register?ref=1116718520), [binance-coin-futures](https://accounts.maxweb.academy/register?ref=1116718520), bitfinex, bitget, bitget-futures, bitmart, bitmex, bitstamp, [bybit](https://www.bybit.com/invite?ref=XNYP2K), coinbase, [cryptocom](https://crypto.com/exch/tqj4b8x48w), deribit, erisx (Cboe Digital), [gateio](https://www.gate.com/signup/VLUQXVFWAW?ref_type=103), [gateio-perpetual-futures](https://www.gate.com/signup/VLUQXVFWAW?ref_type=103), gemini, [huobi](https://www.htx.com/invite/en-us/1f?invite_code=rmw7d223), [huobi-usdt-swap](https://www.htx.com/invite/en-us/1f?invite_code=rmw7d223), [huobi-coin-swap](https://www.htx.com/invite/en-us/1f?invite_code=rmw7d223), [hyperliquid](https://app.hyperliquid.xyz/), kraken, kraken-futures, kucoin, kucoin-futures, mexc, [okx](https://www.okx.com/join/47636709). * FIX: [binance](https://accounts.maxweb.academy/register?ref=1116718520), coinbase, gemini. * Join us on Discord https://discord.gg/b5EKcp9s8T and Medium https://cryptochassis.medium.com. * For any questions, email hello@cryptochassis.com. @@ -1077,6 +1079,35 @@ Subscription subscription("bybit", "BTCUSDT", "MARKET_DEPTH", "", "", {}, "172.3 #### Reduce build time The Pimpl (Pointer to Implementation) idiom in C++ can significantly reduce build time. This reduction is achieved by minimizing compilation dependencies and isolating implementation details. See [this example](example/reduce_build_time). +## Exchange Specific Notes +### Hyperliquid +* If you need execution management, secp256k1 and msgpack are needed. For example, a CMakeLists.txt for macOS will look like +``` +if(APPLE) + find_path(SECP256K1_INCLUDE_DIR secp256k1.h PATHS /opt/homebrew/include) + find_path( + MSGPACK_INCLUDE_DIR + NAMES msgpack.hpp + PATHS /opt/homebrew/include) + + find_library(SECP256K1_LIBRARY secp256k1 PATHS /opt/homebrew/lib) + find_library(MSGPACKC_LIBRARY msgpackc PATHS /opt/homebrew/lib) +endif() + +add_compile_definitions(CCAPI_ENABLE_SERVICE_EXECUTION_MANAGEMENT) +add_compile_definitions(CCAPI_ENABLE_EXCHANGE_HYPERLIQUID) +add_executable(${NAME} main.cpp) +add_dependencies(${NAME} boost rapidjson) + +target_include_directories(${NAME} PRIVATE ${SECP256K1_INCLUDE_DIR} + ${MSGPACK_INCLUDE_DIR}) +target_link_libraries(${NAME} PRIVATE ${SECP256K1_LIBRARY} ${MSGPACKC_LIBRARY}) +``` +* For create order and cancel order, please provide hyperliquid's asset id as instrument (e.g. BTC = 0 on mainnet. See https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/asset-ids). For example, +``` +Request request(Request::Operation::CREATE_ORDER, "hyperliquid", "0"); // Corresponds to https://app.hyperliquid.xyz/trade/BTC +``` + ## Performance Tuning * Turn on compiler optimization flags (e.g. `cmake -DCMAKE_BUILD_TYPE=Release ...`). * Enable link time optimization (e.g. in CMakeLists.txt `set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)` before a target is created). Note that link time optimization is only applicable to static linking. diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 51f7552f..6da4a0ad 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -57,9 +57,10 @@ if(NOT HFFIX_INCLUDE_DIR) else() add_library(hffix INTERFACE) endif() -include_directories( - ${CCAPI_PROJECT_DIR}/include ${BOOST_INCLUDE_DIR} ${RAPIDJSON_INCLUDE_DIR} - ${HFFIX_INCLUDE_DIR} ${OPENSSL_INCLUDE_DIR}) + +include_directories(${CCAPI_PROJECT_DIR}/include) +include_directories(SYSTEM ${BOOST_INCLUDE_DIR} ${RAPIDJSON_INCLUDE_DIR} + ${HFFIX_INCLUDE_DIR} ${OPENSSL_INCLUDE_DIR}) find_package(OpenSSL REQUIRED) set(ADDITIONAL_LINK_LIBRARIES "") diff --git a/include/ccapi_cpp/ccapi_macro.h b/include/ccapi_cpp/ccapi_macro.h index d50df7ce..11495613 100644 --- a/include/ccapi_cpp/ccapi_macro.h +++ b/include/ccapi_cpp/ccapi_macro.h @@ -203,6 +203,10 @@ #define CCAPI_EXCHANGE_NAME_WHITEBIT "whitebit" #endif +#ifndef CCAPI_EXCHANGE_NAME_HYPERLIQUID +#define CCAPI_EXCHANGE_NAME_HYPERLIQUID "hyperliquid" +#endif + #ifndef CCAPI_LAST_PRICE #define CCAPI_LAST_PRICE "LAST_PRICE" #endif @@ -487,6 +491,9 @@ #define CCAPI_WEBSOCKET_MEXC_FUTURES_CHANNEL_DEPTH "depth" #define CCAPI_WEBSOCKET_WHITEBIT_CHANNEL_MARKET_TRADES "trades" #define CCAPI_WEBSOCKET_WHITEBIT_CHANNEL_MARKET_DEPTH "depth" +#define CCAPI_WEBSOCKET_HYPERLIQUID_CHANNEL_BBO "bbo" +#define CCAPI_WEBSOCKET_HYPERLIQUID_CHANNEL_L2_BOOK "l2Book" +#define CCAPI_WEBSOCKET_HYPERLIQUID_CHANNEL_TRADE "trades" #ifndef CCAPI_CHANNEL_ID #define CCAPI_CHANNEL_ID "channelId" #endif @@ -667,6 +674,14 @@ #define CCAPI_EM_POSITION_SIDE "POSITION_SIDE" #endif +#ifndef CCAPI_EM_POSITION_SIDE_LONG +#define CCAPI_EM_POSITION_SIDE_LONG "POSITION_SIDE_LONG" +#endif + +#ifndef CCAPI_EM_POSITION_SIDE_SHORT +#define CCAPI_EM_POSITION_SIDE_SHORT "POSITION_SIDE_SHORT" +#endif + #ifndef CCAPI_EM_POSITION_QUANTITY #define CCAPI_EM_POSITION_QUANTITY "QUANTITY" #endif @@ -1057,6 +1072,10 @@ #define CCAPI_WHITEBIT_URL_REST_BASE "https://whitebit.com" #endif +#ifndef CCAPI_HYPERLIQUID_URL_REST_BASE +#define CCAPI_HYPERLIQUID_URL_REST_BASE "https://api.hyperliquid.xyz" +#endif + // end: exchange REST urls // start: exchange WS urls @@ -1252,6 +1271,10 @@ #define CCAPI_WHITEBIT_URL_WS_BASE "wss://api.whitebit.com" #endif +#ifndef CCAPI_HYPERLIQUID_URL_WS_BASE +#define CCAPI_HYPERLIQUID_URL_WS_BASE "wss://api.hyperliquid.xyz" +#endif + // end: exchange WS urls // start: exchange FIX urls @@ -1641,6 +1664,14 @@ #define CCAPI_MEXC_FUTURES_API_SECRET "MEXC_FUTURES_API_SECRET" #endif +#ifndef CCAPI_HYPERLIQUID_WALLET_KEY +#define CCAPI_HYPERLIQUID_WALLET_KEY "HYPERLIQUID_WALLET_KEY" +#endif + +#ifndef CCAPI_HYPERLIQUID_ACCOUNT_ADDRESS +#define CCAPI_HYPERLIQUID_ACCOUNT_ADDRESS "HYPERLIQUID_ACCOUNT_ADDRESS" +#endif + // end: exchange API credentials #define CCAPI_HTTP_PORT_DEFAULT "80" diff --git a/include/ccapi_cpp/ccapi_request.h b/include/ccapi_cpp/ccapi_request.h index 0c3c2224..6b7dfcf2 100644 --- a/include/ccapi_cpp/ccapi_request.h +++ b/include/ccapi_cpp/ccapi_request.h @@ -294,6 +294,18 @@ class Request { } nextClientOrderId += std::to_string(lastClientOrderIdUnixTimestampInSeconds); nextClientOrderId += UtilString::leftPadTo(std::to_string(lastClientOrderIdSequenceNumber), CCAPI_EM_CLIENT_ORDER_ID_SEQUENCE_NUMBER_PAD_TO_LENGTH, '0'); + + if (this->exchange == CCAPI_EXCHANGE_NAME_HYPERLIQUID) { + int64_t value = std::stoll(nextClientOrderId); + std::stringstream ss; + ss << std::hex << std::nouppercase << value; + std::string hexStr = ss.str(); + if (hexStr.length() < 32) { + hexStr = std::string(32 - hexStr.length(), '0') + hexStr; + } + nextClientOrderId = "0x" + hexStr; + } + return nextClientOrderId; } #ifndef CCAPI_EXPOSE_INTERNAL diff --git a/include/ccapi_cpp/ccapi_session.h b/include/ccapi_cpp/ccapi_session.h index bef7e9f8..0c632aa6 100644 --- a/include/ccapi_cpp/ccapi_session.h +++ b/include/ccapi_cpp/ccapi_session.h @@ -100,6 +100,9 @@ #ifdef CCAPI_ENABLE_EXCHANGE_WHITEBIT #include "ccapi_cpp/service/ccapi_market_data_service_whitebit.h" #endif +#ifdef CCAPI_ENABLE_EXCHANGE_HYPERLIQUID +#include "ccapi_cpp/service/ccapi_market_data_service_hyperliquid.h" +#endif #endif // end: enable exchanges for market data @@ -204,6 +207,9 @@ #ifdef CCAPI_ENABLE_EXCHANGE_WHITEBIT #include "ccapi_cpp/service/ccapi_execution_management_service_whitebit.h" #endif +#ifdef CCAPI_ENABLE_EXCHANGE_HYPERLIQUID +#include "ccapi_cpp/service/ccapi_execution_management_service_hyperliquid.h" +#endif #endif // end: enable exchanges for execution management @@ -303,6 +309,7 @@ class Session { this->serviceContextPtr->start(); } this->onEventFunc = std::bind(&Session::onEvent, this, std::placeholders::_1, std::placeholders::_2); + #ifdef CCAPI_ENABLE_SERVICE_MARKET_DATA #ifdef CCAPI_ENABLE_EXCHANGE_COINBASE this->serviceByServiceNameExchangeMap[CCAPI_MARKET_DATA][CCAPI_EXCHANGE_NAME_COINBASE] = @@ -432,7 +439,12 @@ class Session { this->serviceByServiceNameExchangeMap[CCAPI_MARKET_DATA][CCAPI_EXCHANGE_NAME_WHITEBIT] = std::make_shared(this->onEventFunc, sessionOptions, sessionConfigs, this->serviceContextPtr); #endif +#ifdef CCAPI_ENABLE_EXCHANGE_HYPERLIQUID + this->serviceByServiceNameExchangeMap[CCAPI_MARKET_DATA][CCAPI_EXCHANGE_NAME_HYPERLIQUID] = + std::make_shared(this->onEventFunc, sessionOptions, sessionConfigs, this->serviceContextPtr); #endif +#endif + #ifdef CCAPI_ENABLE_SERVICE_EXECUTION_MANAGEMENT #ifdef CCAPI_ENABLE_EXCHANGE_COINBASE this->serviceByServiceNameExchangeMap[CCAPI_EXECUTION_MANAGEMENT][CCAPI_EXCHANGE_NAME_COINBASE] = @@ -566,6 +578,10 @@ class Session { // this->serviceByServiceNameExchangeMap[CCAPI_EXECUTION_MANAGEMENT][CCAPI_EXCHANGE_NAME_WHITEBIT] = // std::make_shared(this->onEventFunc, sessionOptions, sessionConfigs, this->serviceContextPtr); // #endif +#ifdef CCAPI_ENABLE_EXCHANGE_HYPERLIQUID + this->serviceByServiceNameExchangeMap[CCAPI_EXECUTION_MANAGEMENT][CCAPI_EXCHANGE_NAME_HYPERLIQUID] = + std::make_shared(this->onEventFunc, sessionOptions, sessionConfigs, this->serviceContextPtr); +#endif #endif #ifdef CCAPI_ENABLE_SERVICE_FIX diff --git a/include/ccapi_cpp/ccapi_session_configs.h b/include/ccapi_cpp/ccapi_session_configs.h index 4123bde8..bb3c6864 100644 --- a/include/ccapi_cpp/ccapi_session_configs.h +++ b/include/ccapi_cpp/ccapi_session_configs.h @@ -43,6 +43,10 @@ class SessionConfigs { void setUrlWebsocketBase(const std::map& urlWebsocketBase) { this->urlWebsocketBase = urlWebsocketBase; } + void setUrlWebsocketOrderEntryBase(const std::map& urlWebsocketOrderEntryBase) { + this->urlWebsocketOrderEntryBase = urlWebsocketOrderEntryBase; + } + void setUrlRestBase(const std::map& urlRestBase) { this->urlRestBase = urlRestBase; } void setUrlFixBase(const std::map& urlFixBase) { this->urlFixBase = urlFixBase; } @@ -200,6 +204,10 @@ class SessionConfigs { {CCAPI_TRADE, CCAPI_WEBSOCKET_WHITEBIT_CHANNEL_MARKET_TRADES}, {CCAPI_MARKET_DEPTH, CCAPI_WEBSOCKET_WHITEBIT_CHANNEL_MARKET_DEPTH}, }; + std::map fieldWebsocketChannelMapHyperliquid = { + {CCAPI_MARKET_DEPTH, CCAPI_WEBSOCKET_HYPERLIQUID_CHANNEL_BBO}, + {CCAPI_TRADE, CCAPI_WEBSOCKET_HYPERLIQUID_CHANNEL_TRADE}, + }; for (auto const& fieldWebsocketChannel : fieldWebsocketChannelMapCoinbase) { this->exchangeFieldMap[CCAPI_EXCHANGE_NAME_COINBASE].push_back(fieldWebsocketChannel.first); } @@ -296,6 +304,9 @@ class SessionConfigs { for (auto const& fieldWebsocketChannel : fieldWebsocketChannelMapWhitebit) { this->exchangeFieldMap[CCAPI_EXCHANGE_NAME_WHITEBIT].push_back(fieldWebsocketChannel.first); } + for (auto const& fieldWebsocketChannel : fieldWebsocketChannelMapHyperliquid) { + this->exchangeFieldMap[CCAPI_EXCHANGE_NAME_HYPERLIQUID].push_back(fieldWebsocketChannel.first); + } for (auto& x : this->exchangeFieldMap) { x.second.push_back(CCAPI_GENERIC_PUBLIC_SUBSCRIPTION); } @@ -333,6 +344,7 @@ class SessionConfigs { {CCAPI_EXCHANGE_NAME_MEXC, fieldWebsocketChannelMapMexc}, {CCAPI_EXCHANGE_NAME_MEXC_FUTURES, fieldWebsocketChannelMapMexcFutures}, {CCAPI_EXCHANGE_NAME_WHITEBIT, fieldWebsocketChannelMapWhitebit}, + {CCAPI_EXCHANGE_NAME_HYPERLIQUID, fieldWebsocketChannelMapHyperliquid}, }; this->urlWebsocketBase = { {CCAPI_EXCHANGE_NAME_COINBASE, CCAPI_COINBASE_URL_WS_BASE}, @@ -370,6 +382,7 @@ class SessionConfigs { {CCAPI_EXCHANGE_NAME_MEXC, CCAPI_MEXC_URL_WS_BASE}, {CCAPI_EXCHANGE_NAME_MEXC_FUTURES, CCAPI_MEXC_FUTURES_URL_WS_BASE}, {CCAPI_EXCHANGE_NAME_WHITEBIT, CCAPI_WHITEBIT_URL_WS_BASE}, + {CCAPI_EXCHANGE_NAME_HYPERLIQUID, CCAPI_HYPERLIQUID_URL_WS_BASE}, }; this->urlWebsocketOrderEntryBase = { {CCAPI_EXCHANGE_NAME_OKX, CCAPI_OKX_URL_WS_BASE}, @@ -416,6 +429,7 @@ class SessionConfigs { {CCAPI_EXCHANGE_NAME_MEXC, CCAPI_MEXC_URL_REST_BASE}, {CCAPI_EXCHANGE_NAME_MEXC_FUTURES, CCAPI_MEXC_FUTURES_URL_REST_BASE}, {CCAPI_EXCHANGE_NAME_WHITEBIT, CCAPI_WHITEBIT_URL_REST_BASE}, + {CCAPI_EXCHANGE_NAME_HYPERLIQUID, CCAPI_HYPERLIQUID_URL_REST_BASE}, }; } diff --git a/include/ccapi_cpp/ccapi_util_private.h b/include/ccapi_cpp/ccapi_util_private.h index 3e673b64..a02a0503 100644 --- a/include/ccapi_cpp/ccapi_util_private.h +++ b/include/ccapi_cpp/ccapi_util_private.h @@ -42,6 +42,23 @@ namespace ccapi { */ class UtilString { public: + static std::string trimTrailingZeros(const std::string& input) { + std::string s = input; + + auto pos = s.find('.'); + if (pos == std::string::npos) return s; + + while (!s.empty() && s.back() == '0') { + s.pop_back(); + } + + if (!s.empty() && s.back() == '.') { + s.pop_back(); + } + + return s; + } + static bool startsWith(const std::string& str, const std::string& prefix) { return str.size() >= prefix.size() && std::memcmp(str.data(), prefix.data(), prefix.size()) == 0; } diff --git a/include/ccapi_cpp/service/ccapi_execution_management_service_binance_base.h b/include/ccapi_cpp/service/ccapi_execution_management_service_binance_base.h index c5b0370e..5bb81819 100644 --- a/include/ccapi_cpp/service/ccapi_execution_management_service_binance_base.h +++ b/include/ccapi_cpp/service/ccapi_execution_management_service_binance_base.h @@ -519,6 +519,7 @@ class ExecutionManagementServiceBinanceBase : public ExecutionManagementService message.setElementList(elementList); message.setCorrelationIdList({requestCorrelationId}); } + this->requestCorrelationIdByWsRequestIdByConnectionIdMap.at(wsConnectionPtr->id).erase(wsRequestId); } messageList.emplace_back(std::move(message)); } else { diff --git a/include/ccapi_cpp/service/ccapi_execution_management_service_bybit.h b/include/ccapi_cpp/service/ccapi_execution_management_service_bybit.h index c6d3385d..e251a801 100644 --- a/include/ccapi_cpp/service/ccapi_execution_management_service_bybit.h +++ b/include/ccapi_cpp/service/ccapi_execution_management_service_bybit.h @@ -411,6 +411,7 @@ class ExecutionManagementServiceBybit : public ExecutionManagementService { message.setCorrelationIdList({requestCorrelationId}); messageList.emplace_back(std::move(message)); } + this->requestCorrelationIdByWsRequestIdByConnectionIdMap.at(wsConnectionPtr->id).erase(wsRequestId); } } } else { diff --git a/include/ccapi_cpp/service/ccapi_execution_management_service_hyperliquid.h b/include/ccapi_cpp/service/ccapi_execution_management_service_hyperliquid.h new file mode 100644 index 00000000..428b4a8f --- /dev/null +++ b/include/ccapi_cpp/service/ccapi_execution_management_service_hyperliquid.h @@ -0,0 +1,866 @@ +#pragma once + +#ifdef CCAPI_ENABLE_SERVICE_EXECUTION_MANAGEMENT +#ifdef CCAPI_ENABLE_EXCHANGE_HYPERLIQUID +#include +#include +#include +#include +#include +#include +#include + +#include "ccapi_cpp/service/ccapi_execution_management_service.h" +#include "msgpack.hpp" +#include "secp256k1.h" +#include "secp256k1_recovery.h" + +namespace ccapi { + +class ExecutionManagementServiceHyperliquid : public ExecutionManagementService { + public: + ExecutionManagementServiceHyperliquid(std::function*)> eventHandler, SessionOptions sessionOptions, SessionConfigs sessionConfigs, + ServiceContextPtr serviceContextPtr) + : ExecutionManagementService(eventHandler, sessionOptions, sessionConfigs, serviceContextPtr) { + this->exchangeName = CCAPI_EXCHANGE_NAME_HYPERLIQUID; + this->baseUrlWs = sessionConfigs.getUrlWebsocketBase().at(this->exchangeName) + "/ws"; + this->baseUrlRest = sessionConfigs.getUrlRestBase().at(this->exchangeName); + this->setHostRestFromUrlRest(this->baseUrlRest); + this->walletKeyName = CCAPI_HYPERLIQUID_WALLET_KEY; + this->accountAddressName = CCAPI_HYPERLIQUID_ACCOUNT_ADDRESS; + this->setupCredential({this->walletKeyName, this->accountAddressName}); + this->infoTarget = "/info"; + this->exchangeTarget = "/exchange"; + } + + virtual ~ExecutionManagementServiceHyperliquid() {} +#ifndef CCAPI_EXPOSE_INTERNAL + + private: +#endif + + static uint64_t rol64(uint64_t a, int offset) { return (a << offset) | (a >> (64 - offset)); } + + static constexpr uint64_t keccakf_rndc[24] = { + 0x0000000000000001ULL, 0x0000000000008082ULL, 0x800000000000808aULL, 0x8000000080008000ULL, 0x000000000000808bULL, 0x0000000080000001ULL, + 0x8000000080008081ULL, 0x8000000000008009ULL, 0x000000000000008aULL, 0x0000000000000088ULL, 0x0000000080008009ULL, 0x000000008000000aULL, + 0x000000008000808bULL, 0x800000000000008bULL, 0x8000000000008089ULL, 0x8000000000008003ULL, 0x8000000000008002ULL, 0x8000000000000080ULL, + 0x000000000000800aULL, 0x800000008000000aULL, 0x8000000080008081ULL, 0x8000000000008080ULL, 0x0000000080000001ULL, 0x8000000080008008ULL}; + + static constexpr int keccakf_rotc[24] = {1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44}; + + static constexpr int keccakf_piln[24] = {10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1}; + + void keccakf(uint64_t st[25]) { + uint64_t t, bc[5]; + for (int round = 0; round < 24; round++) { + for (int i = 0; i < 5; i++) bc[i] = st[i] ^ st[i + 5] ^ st[i + 10] ^ st[i + 15] ^ st[i + 20]; + for (int i = 0; i < 5; i++) { + t = bc[(i + 4) % 5] ^ this->rol64(bc[(i + 1) % 5], 1); + for (int j = 0; j < 25; j += 5) st[j + i] ^= t; + } + t = st[1]; + for (int i = 0; i < 24; i++) { + int j = keccakf_piln[i]; + bc[0] = st[j]; + st[j] = this->rol64(t, keccakf_rotc[i]); + t = bc[0]; + } + for (int j = 0; j < 25; j += 5) { + for (int i = 0; i < 5; i++) bc[i] = st[j + i]; + for (int i = 0; i < 5; i++) st[j + i] ^= (~bc[(i + 1) % 5]) & bc[(i + 2) % 5]; + } + st[0] ^= keccakf_rndc[round]; + } + } + + std::vector keccak256_raw(const std::vector& data) { + uint64_t st[25] = {0}; + uint8_t temp[144]; + size_t rsiz = 136; + size_t pt = 0; + for (size_t i = 0; i < data.size(); i++) { + temp[pt++] = data[i]; + if (pt >= rsiz) { + for (size_t j = 0; j < rsiz / 8; j++) { + uint64_t val = 0; + for (int k = 0; k < 8; k++) { + val |= ((uint64_t)temp[j * 8 + k]) << (8 * k); + } + st[j] ^= val; + } + this->keccakf(st); + pt = 0; + } + } + temp[pt++] = 0x01; + while (pt < rsiz) { + temp[pt++] = 0x00; + } + temp[rsiz - 1] |= 0x80; + for (size_t j = 0; j < rsiz / 8; j++) { + uint64_t val = 0; + for (int k = 0; k < 8; k++) { + val |= ((uint64_t)temp[j * 8 + k]) << (8 * k); + } + st[j] ^= val; + } + this->keccakf(st); + std::vector hash(32); + for (size_t i = 0; i < 32; i++) { + hash[i] = (st[i / 8] >> (8 * (i % 8))) & 0xFF; + } + return hash; + } + + std::string toHex(const std::vector& v) { + std::ostringstream oss; + oss << "0x"; + for (auto b : v) { + oss << std::hex << std::setw(2) << std::setfill('0') << (int)b; + } + return oss.str(); + } + + std::string deriveEthAddress(const std::array& privkey) { + secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); + + secp256k1_pubkey pubkey; + + if (!secp256k1_ec_pubkey_create(ctx, &pubkey, privkey.data())) { + secp256k1_context_destroy(ctx); + throw std::runtime_error("pubkey derivation failed"); + } + + uint8_t serialized[65]; + size_t len = 65; + + secp256k1_ec_pubkey_serialize(ctx, serialized, &len, &pubkey, SECP256K1_EC_UNCOMPRESSED); + + secp256k1_context_destroy(ctx); + + std::vector pub(serialized + 1, serialized + 65); + + auto hash = this->keccak256_raw(pub); + + std::vector addr(hash.end() - 20, hash.end()); + + return this->toHex(addr); + } + + bool doesHttpBodyContainError(boost::beast::string_view bodyView) override { return bodyView.find("\"status\":\"err\"") != boost::beast::string_view::npos; } + + void pingOnApplicationLevel(std::shared_ptr wsConnectionPtr, ErrorCode& ec) override { + this->send(wsConnectionPtr, R"({"method":"ping"})", ec); + } + + void convertRequestForRest(http::request& req, const Request& request, const TimePoint& now, const std::string& symbolId, + const std::map& credential) override { + Request::Operation operation = request.getOperation(); + switch (operation) { + case Request::Operation::GENERIC_PRIVATE_REQUEST: { + ExecutionManagementService::convertRequestForRestGenericPrivateRequest(req, request, now, symbolId, credential); + } break; + case Request::Operation::CREATE_ORDER: { + req.method(http::verb::post); + req.target(this->exchangeTarget); + req.set(http::field::content_type, "application/json"); + const auto& param = request.getFirstParamWithDefault(); + + rapidjson::Document actionJson; + actionJson.SetObject(); + auto& allocator = actionJson.GetAllocator(); + actionJson.AddMember("type", "order", allocator); + rapidjson::Value orders(rapidjson::kArrayType); + orders.PushBack(this->buildOrderWire(param, allocator, symbolId), allocator); + actionJson.AddMember("orders", orders, allocator); + actionJson.AddMember("grouping", "na", allocator); + + std::vector actionBytes = this->buildMsgpackActionForCreateOrder(param, symbolId); + + uint64_t nonce = std::chrono::duration_cast(now.time_since_epoch()).count(); + + auto privkey = hexToBytes32(mapGetWithDefault(credential, this->walletKeyName)); + + auto sig = this->signL1Action(actionBytes, std::nullopt, nonce, std::nullopt, true, privkey); + + rapidjson::Document body; + body.SetObject(); + auto& a = body.GetAllocator(); + body.AddMember("action", actionJson, a); + body.AddMember("nonce", nonce, a); + rapidjson::Value sigObj(rapidjson::kObjectType); + sigObj.AddMember("r", rapidjson::Value(bytesToHex_2(sig.r).c_str(), a), a); + sigObj.AddMember("s", rapidjson::Value(bytesToHex_2(sig.s).c_str(), a), a); + sigObj.AddMember("v", sig.v, a); + body.AddMember("signature", sigObj, a); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + body.Accept(writer); + req.body() = buffer.GetString(); + req.prepare_payload(); + } break; + case Request::Operation::CANCEL_ORDER: { + req.method(http::verb::post); + req.target(this->exchangeTarget); + req.set(http::field::content_type, "application/json"); + + const auto& param = request.getFirstParamWithDefault(); + + rapidjson::Document actionJson; + actionJson.SetObject(); + auto& allocator = actionJson.GetAllocator(); + + actionJson.AddMember("type", "cancel", allocator); + + rapidjson::Value cancels(rapidjson::kArrayType); + + rapidjson::Value cancelObj(rapidjson::kObjectType); + cancelObj.AddMember("a", std::stoi(symbolId), allocator); + cancelObj.AddMember("o", rapidjson::Value().SetUint64(std::stoull(param.at(CCAPI_EM_ORDER_ID))), allocator); + + cancels.PushBack(cancelObj, allocator); + + actionJson.AddMember("cancels", cancels, allocator); + + std::vector actionBytes = this->buildMsgpackActionForCancelOrder(param, symbolId); + + uint64_t nonce = std::chrono::duration_cast(now.time_since_epoch()).count(); + + auto privkey = hexToBytes32(mapGetWithDefault(credential, this->walletKeyName)); + + auto sig = this->signL1Action(actionBytes, std::nullopt, nonce, std::nullopt, true, privkey); + + rapidjson::Document body; + body.SetObject(); + auto& a = body.GetAllocator(); + + body.AddMember("action", actionJson, a); + body.AddMember("nonce", nonce, a); + + rapidjson::Value sigObj(rapidjson::kObjectType); + sigObj.AddMember("r", rapidjson::Value(bytesToHex_2(sig.r).c_str(), a), a); + sigObj.AddMember("s", rapidjson::Value(bytesToHex_2(sig.s).c_str(), a), a); + sigObj.AddMember("v", sig.v, a); + + body.AddMember("signature", sigObj, a); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + body.Accept(writer); + + req.body() = buffer.GetString(); + req.prepare_payload(); + } break; + case Request::Operation::GET_OPEN_ORDERS: { + req.method(http::verb::post); + req.target(this->infoTarget); + + rapidjson::Document body; + body.SetObject(); + auto& a = body.GetAllocator(); + body.AddMember("type", "frontendOpenOrders", a); + body.AddMember("user", rapidjson::Value(this->resolveUserAddress(credential).c_str(), a), a); + req.set(http::field::content_type, "application/json"); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + body.Accept(writer); + + req.body() = buffer.GetString(); + req.prepare_payload(); + + } break; + case Request::Operation::GET_ACCOUNT_BALANCES: { + req.method(http::verb::post); + req.target(this->infoTarget); + + rapidjson::Document body; + body.SetObject(); + auto& a = body.GetAllocator(); + body.AddMember("type", "spotClearinghouseState", a); + body.AddMember("user", rapidjson::Value(this->resolveUserAddress(credential).c_str(), a), a); + req.set(http::field::content_type, "application/json"); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + body.Accept(writer); + + req.body() = buffer.GetString(); + req.prepare_payload(); + } break; + case Request::Operation::GET_ACCOUNT_POSITIONS: { + req.method(http::verb::post); + req.target(this->infoTarget); + + rapidjson::Document body; + body.SetObject(); + auto& a = body.GetAllocator(); + body.AddMember("type", "clearinghouseState", a); + body.AddMember("user", rapidjson::Value(this->resolveUserAddress(credential).c_str(), a), a); + req.set(http::field::content_type, "application/json"); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + body.Accept(writer); + + req.body() = buffer.GetString(); + req.prepare_payload(); + } break; + + default: + this->convertRequestForRestCustom(req, request, now, symbolId, credential); + } + } + + std::string resolveUserAddress(const std::map& credential) { + const std::string& providedAddress = mapGetWithDefault(credential, this->accountAddressName); + + if (!providedAddress.empty()) return providedAddress; + + const auto privateKey = hexToBytes32(mapGetWithDefault(credential, walletKeyName)); + + return deriveEthAddress(privateKey); + } + + rapidjson::Value buildOrderWire(const std::map& param, rapidjson::Document::AllocatorType& allocator, const std::string& symbolId) { + rapidjson::Value order(rapidjson::kObjectType); + order.AddMember("a", std::stoi(symbolId), allocator); + order.AddMember("b", param.at(CCAPI_EM_ORDER_SIDE) == CCAPI_EM_ORDER_SIDE_BUY, allocator); + order.AddMember("p", rapidjson::Value(UtilString::trimTrailingZeros(param.at(CCAPI_EM_ORDER_LIMIT_PRICE)).c_str(), allocator), allocator); + order.AddMember("s", rapidjson::Value(UtilString::trimTrailingZeros(param.at(CCAPI_EM_ORDER_QUANTITY)).c_str(), allocator), allocator); + order.AddMember("r", false, allocator); + if (param.find(CCAPI_EM_CLIENT_ORDER_ID) != param.end()) { + order.AddMember("c", rapidjson::Value(param.at(CCAPI_EM_CLIENT_ORDER_ID).c_str(), allocator), allocator); + } + rapidjson::Value t(rapidjson::kObjectType); + rapidjson::Value limit(rapidjson::kObjectType); + limit.AddMember("tif", "Gtc", allocator); + t.AddMember("limit", limit, allocator); + order.AddMember("t", t, allocator); + return order; + } + + std::vector buildMsgpackActionForCreateOrder(const std::map& param, const std::string& symbolId) { + msgpack::sbuffer sbuf; + msgpack::packer pk(&sbuf); + + pk.pack_map(3); + + pk.pack(std::string("type")); + pk.pack(std::string("order")); + + pk.pack(std::string("orders")); + pk.pack_array(1); + + int fieldCount = 6; + if (param.find(CCAPI_EM_CLIENT_ORDER_ID) != param.end()) { + fieldCount += 1; + } + + pk.pack_map(fieldCount); + + pk.pack(std::string("a")); + pk.pack(std::stoi(symbolId)); + + pk.pack(std::string("b")); + pk.pack(param.at(CCAPI_EM_ORDER_SIDE) == CCAPI_EM_ORDER_SIDE_BUY); + + pk.pack(std::string("p")); + pk.pack(UtilString::trimTrailingZeros(param.at(CCAPI_EM_ORDER_LIMIT_PRICE))); + + pk.pack(std::string("s")); + pk.pack(UtilString::trimTrailingZeros(param.at(CCAPI_EM_ORDER_QUANTITY))); + + pk.pack(std::string("r")); + pk.pack(false); + + pk.pack(std::string("t")); + pk.pack_map(1); + + pk.pack(std::string("limit")); + pk.pack_map(1); + + pk.pack(std::string("tif")); + pk.pack(std::string("Gtc")); + + if (param.find(CCAPI_EM_CLIENT_ORDER_ID) != param.end()) { + pk.pack(std::string("c")); + pk.pack(param.at(CCAPI_EM_CLIENT_ORDER_ID)); + } + + pk.pack(std::string("grouping")); + pk.pack(std::string("na")); + + return std::vector(sbuf.data(), sbuf.data() + sbuf.size()); + } + + std::vector buildMsgpackActionForCancelOrder(const std::map& param, const std::string& symbolId) { + msgpack::sbuffer sbuf; + msgpack::packer pk(&sbuf); + + pk.pack_map(2); + + pk.pack(std::string("type")); + pk.pack(std::string("cancel")); + + pk.pack(std::string("cancels")); + pk.pack_array(1); + + pk.pack_map(2); + + pk.pack(std::string("a")); + pk.pack(std::stoi(symbolId)); + + pk.pack(std::string("o")); + pk.pack(std::stoull(param.at(CCAPI_EM_ORDER_ID))); + + return std::vector(sbuf.data(), sbuf.data() + sbuf.size()); + } + + struct Signature { + std::array r; + std::array s; + int v; + }; + + struct Agent { + std::string source; + std::array connectionId; + }; + + Signature signL1Action(const std::vector& actionBytes, const std::optional& vault, uint64_t nonce, std::optional expiresAfter, + bool isMainnet, const std::array& privkey) { + auto hash = this->actionHash(actionBytes, vault, nonce, expiresAfter); + + Agent agent; + agent.source = isMainnet ? "a" : "b"; + agent.connectionId = hash; + + auto digest = this->eip712Hash(agent); + + return this->signHash(digest, privkey); + } + + std::array actionHash(const std::vector& actionBytes, const std::optional& vault, uint64_t nonce, + const std::optional& expiresAfter) { + auto hash = this->keccak256(actionBytes); + + std::vector data = actionBytes; + + this->appendU64(data, nonce); + + data.push_back(vault ? 0x01 : 0x00); + + if (vault) { + auto addr = this->hexToBytes(vault.value()); + data.insert(data.end(), addr.begin(), addr.end()); + } + + if (expiresAfter) { + data.push_back(0x00); + this->appendU64(data, *expiresAfter); + } + + return this->keccak256(data); + } + + std::vector packMsgpack(const msgpack::type::variant& v) { + msgpack::sbuffer sbuf; + msgpack::pack(sbuf, v); + return {sbuf.data(), sbuf.data() + sbuf.size()}; + } + + void appendU64(std::vector& v, uint64_t x) { + for (int i = 7; i >= 0; i--) v.push_back((x >> (8 * i)) & 0xff); + } + + std::array hexToBytes32(const std::string& hex) { + std::array out{}; + std::string h = hex.substr(0, 2) == "0x" ? hex.substr(2) : hex; + for (int i = 0; i < 32; i++) out[i] = std::stoi(h.substr(i * 2, 2), nullptr, 16); + return out; + } + + std::vector hexToBytes(const std::string& hex) { + std::string h = hex.substr(0, 2) == "0x" ? hex.substr(2) : hex; + std::vector out(h.size() / 2); + for (size_t i = 0; i < out.size(); i++) out[i] = std::stoi(h.substr(2 * i, 2), nullptr, 16); + return out; + } + + std::string bytesToHex_2(const std::array& data) { + std::ostringstream oss; + oss << std::hex << std::setfill('0'); + + for (uint8_t b : data) { + oss << std::setw(2) << static_cast(b); + } + + return oss.str(); + } + + std::string bytesToHex(const std::vector& data) { + std::ostringstream oss; + oss << std::hex << std::setfill('0'); + + for (uint8_t b : data) { + oss << std::setw(2) << static_cast(b); + } + + return oss.str(); + } + + std::array keccak256(const std::vector& data) { + std::vector hash = keccak256_raw(data); + + std::array out{}; + std::copy(hash.begin(), hash.end(), out.begin()); + + return out; + } + + std::array keccak256Str(const std::string& s) { + std::vector v(s.begin(), s.end()); + return this->keccak256(v); + } + + std::array eip712Hash(const Agent& agent) { + auto keccak_str = [&](const std::string& s) { return this->keccak256Str(s); }; + + auto pad32 = [](const std::vector& v) { + std::vector out(32, 0); + std::copy(v.begin(), v.end(), out.begin() + (32 - v.size())); + return out; + }; + + auto domainTypeHash = keccak_str("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + auto nameHash = keccak_str("Exchange"); + auto versionHash = keccak_str("1"); + + std::vector chainId(32, 0); + uint64_t chainIdValue = 1337; + for (int i = 0; i < 8; i++) { + chainId[31 - i] = (chainIdValue >> (8 * i)) & 0xff; + } + + std::vector verifyingContract(32, 0); + + std::vector domainEncoded; + domainEncoded.insert(domainEncoded.end(), domainTypeHash.begin(), domainTypeHash.end()); + domainEncoded.insert(domainEncoded.end(), nameHash.begin(), nameHash.end()); + domainEncoded.insert(domainEncoded.end(), versionHash.begin(), versionHash.end()); + domainEncoded.insert(domainEncoded.end(), chainId.begin(), chainId.end()); + domainEncoded.insert(domainEncoded.end(), verifyingContract.begin(), verifyingContract.end()); + + auto domainSeparator = this->keccak256(domainEncoded); + + auto agentTypeHash = keccak_str("Agent(string source,bytes32 connectionId)"); + auto sourceHash = keccak_str(agent.source); + + std::vector agentEncoded; + agentEncoded.insert(agentEncoded.end(), agentTypeHash.begin(), agentTypeHash.end()); + agentEncoded.insert(agentEncoded.end(), sourceHash.begin(), sourceHash.end()); + agentEncoded.insert(agentEncoded.end(), agent.connectionId.begin(), agent.connectionId.end()); + + auto structHash = this->keccak256(agentEncoded); + + std::vector finalData = {0x19, 0x01}; + finalData.insert(finalData.end(), domainSeparator.begin(), domainSeparator.end()); + finalData.insert(finalData.end(), structHash.begin(), structHash.end()); + + return this->keccak256(finalData); + } + + Signature signHash(const std::array& hash, const std::array& privkey) { + secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN); + + secp256k1_ecdsa_recoverable_signature sig; + + int ok = secp256k1_ecdsa_sign_recoverable(ctx, &sig, hash.data(), privkey.data(), nullptr, nullptr); + + if (!ok) { + throw std::runtime_error("secp256k1 signing failed"); + } + + uint8_t compact[64]; + int recid = 0; + + secp256k1_ecdsa_recoverable_signature_serialize_compact(ctx, compact, &recid, &sig); + + Signature out; + memcpy(out.r.data(), compact, 32); + memcpy(out.s.data(), compact + 32, 32); + + out.v = recid + 27; + + secp256k1_context_destroy(ctx); + + return out; + } + + void extractOrderInfoFromRequest(std::vector& elementList, const Request& request, const Request::Operation operation, + const rapidjson::Document& document) override { + if (operation == Request::Operation::CREATE_ORDER) { + const auto& response = document["response"]; + + const auto& data = response["data"]; + + const auto& statuses = data["statuses"]; + + for (const auto& s : statuses.GetArray()) { + Element element; + + if (s.HasMember("resting")) { + const auto& r = s["resting"]; + + if (r.HasMember("oid")) { + element.insert(CCAPI_EM_ORDER_ID, r["oid"].GetString()); + } + + element.insert(CCAPI_EM_ORDER_STATUS, "open"); + } + + else if (s.HasMember("filled")) { + const auto& f = s["filled"]; + + if (f.HasMember("oid")) { + element.insert(CCAPI_EM_ORDER_ID, f["oid"].GetString()); + } + + if (f.HasMember("totalSz")) { + element.insert(CCAPI_EM_ORDER_CUMULATIVE_FILLED_QUANTITY, f["totalSz"].GetString()); + } + + if (f.HasMember("avgPx")) { + element.insert(CCAPI_EM_ORDER_AVERAGE_FILLED_PRICE, f["avgPx"].GetString()); + } + + element.insert(CCAPI_EM_ORDER_STATUS, "filled"); + } + + elementList.emplace_back(std::move(element)); + } + } else if (operation == Request::Operation::GET_OPEN_ORDERS) { + if (document.IsArray()) { + for (const auto& x : document.GetArray()) { + Element element; + + if (x.HasMember("oid")) { + element.insert(CCAPI_EM_ORDER_ID, x["oid"].GetString()); + } + + if (x.HasMember("cloid") && !x["cloid"].IsNull()) { + element.insert(CCAPI_EM_CLIENT_ORDER_ID, x["cloid"].GetString()); + } + + if (x.HasMember("coin")) { + element.insert(CCAPI_EM_ORDER_INSTRUMENT, x["coin"].GetString()); + } + + if (x.HasMember("side")) { + std::string side = x["side"].GetString(); + element.insert(CCAPI_EM_ORDER_SIDE, side == "B" ? CCAPI_EM_ORDER_SIDE_BUY : CCAPI_EM_ORDER_SIDE_SELL); + } + + if (x.HasMember("origSz")) { + element.insert(CCAPI_EM_ORDER_QUANTITY, x["origSz"].GetString()); + } + + if (x.HasMember("limitPx")) { + element.insert(CCAPI_EM_ORDER_LIMIT_PRICE, x["limitPx"].GetString()); + } + + element.insert(CCAPI_EM_ORDER_STATUS, "open"); + + elementList.emplace_back(std::move(element)); + } + } + } + } + + void extractAccountInfoFromRequest(std::vector& elementList, const Request& request, const Request::Operation operation, + const rapidjson::Document& document) override { + switch (request.getOperation()) { + case Request::Operation::GET_ACCOUNT_BALANCES: { + const auto& balances = document["balances"]; + for (const auto& x : balances.GetArray()) { + Element element; + element.insert(CCAPI_EM_ASSET, x["coin"].GetString()); + element.insert(CCAPI_EM_QUANTITY_TOTAL, x["total"].GetString()); + elementList.emplace_back(std::move(element)); + } + + } break; + case Request::Operation::GET_ACCOUNT_POSITIONS: { + const auto& positions = document["assetPositions"]; + for (const auto& x : positions.GetArray()) { + Element element; + + const auto& pos = x["position"]; + element.insert(CCAPI_INSTRUMENT, pos["coin"].GetString()); + std::string sizeStr = pos["szi"].GetString(); + double size = std::stod(sizeStr); + element.insert(CCAPI_EM_POSITION_SIDE, size > 0 ? CCAPI_EM_POSITION_SIDE_LONG : CCAPI_EM_POSITION_SIDE_SHORT); + element.insert(CCAPI_EM_POSITION_QUANTITY, sizeStr); + std::string marginType = pos["leverage"]["type"].GetString(); + element.insert(CCAPI_EM_POSITION_MARGIN_TYPE, marginType == "cross" ? CCAPI_EM_MARGIN_TYPE_CROSS_MARGIN : CCAPI_EM_MARGIN_TYPE_ISOLATED_MARGIN); + element.insert(CCAPI_EM_POSITION_ENTRY_PRICE, pos["entryPx"].GetString()); + element.insert(CCAPI_EM_POSITION_LEVERAGE, pos["leverage"]["value"].GetString()); + elementList.emplace_back(std::move(element)); + } + } break; + default: + CCAPI_LOGGER_FATAL(CCAPI_UNSUPPORTED_VALUE); + } + } + + void extractOrderInfo(Element& element, const rapidjson::Value& x, + const std::map>& extractionFieldNameMap, + const std::map> conversionMap = {}) override { + ExecutionManagementService::extractOrderInfo(element, x, extractionFieldNameMap); + } + + std::vector createSendStringListFromSubscription(std::shared_ptr wsConnectionPtr, const Subscription& subscription, + const TimePoint& now, const std::map& credential) override { + std::vector sendStringList; + const auto& fieldSet = subscription.getFieldSet(); + const auto& user = this->resolveUserAddress(credential); + if (fieldSet.find(CCAPI_EM_ORDER_UPDATE) != fieldSet.end()) { + rj::Document document; + document.SetObject(); + auto& allocator = document.GetAllocator(); + document.AddMember("method", rj::Value("subscribe").Move(), allocator); + rj::Value subscriptionObject; + subscriptionObject.SetObject(); + subscriptionObject.AddMember("type", "orderUpdates", allocator); + subscriptionObject.AddMember("user", rj::Value(user.c_str(), allocator).Move(), allocator); + + document.AddMember("subscription", subscriptionObject, allocator); + rj::StringBuffer stringBuffer; + rj::Writer writer(stringBuffer); + document.Accept(writer); + sendStringList.push_back(stringBuffer.GetString()); + } + + if (fieldSet.find(CCAPI_EM_PRIVATE_TRADE) != fieldSet.end()) { + rj::Document document; + document.SetObject(); + auto& allocator = document.GetAllocator(); + document.AddMember("method", rj::Value("subscribe").Move(), allocator); + rj::Value subscriptionObject; + subscriptionObject.SetObject(); + subscriptionObject.AddMember("type", "userFills", allocator); + subscriptionObject.AddMember("user", rj::Value(user.c_str(), allocator).Move(), allocator); + + document.AddMember("subscription", subscriptionObject, allocator); + rj::StringBuffer stringBuffer; + rj::Writer writer(stringBuffer); + document.Accept(writer); + sendStringList.push_back(stringBuffer.GetString()); + } + + return sendStringList; + } + + void onTextMessage(std::shared_ptr wsConnectionPtr, const Subscription& subscription, boost::beast::string_view textMessageView, + const TimePoint& timeReceived) override { + this->jsonDocumentAllocator.Clear(); + rapidjson::Document document(&this->jsonDocumentAllocator); + document.Parse(textMessageView.data(), textMessageView.size()); + std::string channel = document["channel"].GetString(); + Event event = this->createEvent(wsConnectionPtr, subscription, textMessageView, document, channel, timeReceived); + if (!event.getMessageList().empty()) { + this->eventHandler(event, nullptr); + } + } + + Event createEvent(std::shared_ptr wsConnectionPtr, const Subscription& subscription, const std::string& textMessageView, + const rapidjson::Document& document, const std::string& channel, const TimePoint& timeReceived) { + Event event; + + if (channel == "pong") { + return event; + } + + std::vector messageList; + Message message; + message.setTimeReceived(timeReceived); + const auto& correlationId = subscription.getCorrelationId(); + const auto& fieldSet = subscription.getFieldSet(); + + message.setCorrelationIdList({correlationId}); + const rapidjson::Value& data = document["data"]; + + if (channel == "subscriptionResponse") { + event.setType(Event::Type::SUBSCRIPTION_STATUS); + message.setType(Message::Type::SUBSCRIPTION_STARTED); + message.setCorrelationIdList({correlationId}); + Element element; + element.insert(CCAPI_INFO_MESSAGE, textMessageView); + message.setElementList({element}); + messageList.emplace_back(std::move(message)); + } else { + event.setType(Event::Type::SUBSCRIPTION_DATA); + if (channel == "orderUpdates") { + for (const auto& x : data.GetArray()) { + Message message; + message.setTimeReceived(timeReceived); + message.setCorrelationIdList({subscription.getCorrelationId()}); + message.setTime(UtilTime::makeTimePointFromMilliseconds(std::stoll(std::string(x["statusTimestamp"].GetString())))); + message.setType(Message::Type::EXECUTION_MANAGEMENT_EVENTS_ORDER_UPDATE); + const std::map>& extractionFieldNameMap = { + {CCAPI_EM_ORDER_ID, std::make_pair("oid", JsonDataType::STRING)}, + {CCAPI_EM_CLIENT_ORDER_ID, std::make_pair("cloid", JsonDataType::STRING)}, + {CCAPI_EM_ORDER_LIMIT_PRICE, std::make_pair("limitPx", JsonDataType::STRING)}, + {CCAPI_EM_ORDER_QUANTITY, std::make_pair("origSz", JsonDataType::STRING)}, + {CCAPI_EM_ORDER_CUMULATIVE_FILLED_QUANTITY, std::make_pair("accFillSz", JsonDataType::STRING)}, + {CCAPI_EM_ORDER_INSTRUMENT, std::make_pair("coin", JsonDataType::STRING)}, + }; + Element info; + this->extractOrderInfo(info, x["order"], extractionFieldNameMap); + info.insert(CCAPI_EM_ORDER_SIDE, std::string_view(x["order"]["side"].GetString()) == "B" ? CCAPI_EM_ORDER_SIDE_BUY : CCAPI_EM_ORDER_SIDE_SELL); + info.insert(CCAPI_EM_ORDER_STATUS, x["status"].GetString()); + std::vector elementList; + elementList.emplace_back(std::move(info)); + message.setElementList(elementList); + messageList.emplace_back(std::move(message)); + } + + } else if (channel == "userFills") { + for (const auto& x : data["fills"].GetArray()) { + Message message; + message.setTimeReceived(timeReceived); + message.setCorrelationIdList({subscription.getCorrelationId()}); + message.setTime(UtilTime::makeTimePointFromMilliseconds(std::stoll(std::string(x["time"].GetString())))); + message.setType(Message::Type::EXECUTION_MANAGEMENT_EVENTS_PRIVATE_TRADE); + std::vector elementList; + Element element; + element.insert(CCAPI_TRADE_ID, x["tid"].GetString()); + element.insert(CCAPI_EM_ORDER_LAST_EXECUTED_PRICE, x["px"].GetString()); + element.insert(CCAPI_EM_ORDER_LAST_EXECUTED_SIZE, x["sz"].GetString()); + element.insert(CCAPI_EM_ORDER_SIDE, std::string_view(x["side"].GetString()) == "B" ? CCAPI_EM_ORDER_SIDE_BUY : CCAPI_EM_ORDER_SIDE_SELL); + element.insert(CCAPI_IS_MAKER, x["crossed"].GetBool() ? "0" : "1"); + element.insert(CCAPI_EM_ORDER_ID, x["oid"].GetString()); + element.insert(CCAPI_EM_ORDER_INSTRUMENT, x["coin"].GetString()); + element.insert(CCAPI_EM_ORDER_FEE_QUANTITY, x["fee"].GetString()); + element.insert(CCAPI_EM_ORDER_FEE_ASSET, x["feeToken"].GetString()); + elementList.emplace_back(std::move(element)); + message.setElementList(elementList); + messageList.emplace_back(std::move(message)); + } + } + } + + event.setMessageList(messageList); + return event; + } + + std::string infoTarget; + std::string exchangeTarget; + std::string walletKeyName; + std::string walletAddressName; + std::string accountAddressName; +}; + +} // namespace ccapi +#endif +#endif diff --git a/include/ccapi_cpp/service/ccapi_market_data_service_hyperliquid.h b/include/ccapi_cpp/service/ccapi_market_data_service_hyperliquid.h new file mode 100644 index 00000000..a2f02ab5 --- /dev/null +++ b/include/ccapi_cpp/service/ccapi_market_data_service_hyperliquid.h @@ -0,0 +1,300 @@ +#pragma once + +#ifdef CCAPI_ENABLE_SERVICE_MARKET_DATA +#ifdef CCAPI_ENABLE_EXCHANGE_HYPERLIQUID + +#include "ccapi_cpp/service/ccapi_market_data_service.h" + +namespace ccapi { + +class MarketDataServiceHyperliquid : public MarketDataService { + public: + MarketDataServiceHyperliquid(std::function*)> eventHandler, SessionOptions sessionOptions, SessionConfigs sessionConfigs, + ServiceContext* serviceContextPtr) + : MarketDataService(eventHandler, sessionOptions, sessionConfigs, serviceContextPtr) { + this->exchangeName = CCAPI_EXCHANGE_NAME_HYPERLIQUID; + this->baseUrlWs = sessionConfigs.getUrlWebsocketBase().at(this->exchangeName) + "/ws"; + this->baseUrlRest = sessionConfigs.getUrlRestBase().at(this->exchangeName); + this->setHostRestFromUrlRest(this->baseUrlRest); + this->infoTarget = "/info"; + } + + virtual ~MarketDataServiceHyperliquid() {} +#ifndef CCAPI_EXPOSE_INTERNAL + + private: +#endif + + void prepareSubscriptionDetail(std::string& channelId, std::string& symbolId, const std::string& field, std::shared_ptr wsConnectionPtr, + const Subscription& subscription, const std::map optionMap) override { + auto marketDepthRequested = std::stoi(optionMap.at(CCAPI_MARKET_DEPTH_MAX)); + if (field == CCAPI_MARKET_DEPTH) { + if (marketDepthRequested == 1) { + channelId = CCAPI_WEBSOCKET_HYPERLIQUID_CHANNEL_BBO; + } else { + channelId = CCAPI_WEBSOCKET_HYPERLIQUID_CHANNEL_L2_BOOK; + } + } + } + + void pingOnApplicationLevel(std::shared_ptr wsConnectionPtr, ErrorCode& ec) override { + this->send(wsConnectionPtr, R"({"method":"ping"})", ec); + } + + std::vector createSendStringList(std::shared_ptr wsConnectionPtr) override { + std::vector sendStringList; + + for (const auto& subscriptionListByChannelIdSymbolId : this->subscriptionListByConnectionIdChannelIdSymbolIdMap.at(wsConnectionPtr->id)) { + auto channelId = subscriptionListByChannelIdSymbolId.first; + + for (const auto& subscriptionListBySymbolId : subscriptionListByChannelIdSymbolId.second) { + std::string symbolId = subscriptionListBySymbolId.first; + + rj::Document document; + document.SetObject(); + auto& allocator = document.GetAllocator(); + + document.AddMember("method", rj::Value("subscribe", allocator), allocator); + + rj::Value subscription(rj::kObjectType); + + if (channelId == CCAPI_WEBSOCKET_HYPERLIQUID_CHANNEL_BBO) { + this->l2UpdateIsReplaceByConnectionIdChannelIdSymbolIdMap[wsConnectionPtr->id][channelId][symbolId] = true; + } else if (channelId == CCAPI_WEBSOCKET_HYPERLIQUID_CHANNEL_L2_BOOK) { + this->l2UpdateIsReplaceByConnectionIdChannelIdSymbolIdMap[wsConnectionPtr->id][channelId][symbolId] = true; + } + + subscription.AddMember("type", rj::Value(channelId.c_str(), allocator), allocator); + subscription.AddMember("coin", rj::Value(symbolId.c_str(), allocator), allocator); + + document.AddMember("subscription", subscription, allocator); + + rj::StringBuffer stringBuffer; + rj::Writer writer(stringBuffer); + document.Accept(writer); + + sendStringList.push_back(stringBuffer.GetString()); + + std::string exchangeSubscriptionId = channelId + ":" + symbolId; + this->channelIdSymbolIdByConnectionIdExchangeSubscriptionIdMap[wsConnectionPtr->id][exchangeSubscriptionId][CCAPI_CHANNEL_ID] = channelId; + this->channelIdSymbolIdByConnectionIdExchangeSubscriptionIdMap[wsConnectionPtr->id][exchangeSubscriptionId][CCAPI_SYMBOL_ID] = symbolId; + } + } + + return sendStringList; + } + + void processTextMessage(std::shared_ptr wsConnectionPtr, boost::beast::string_view textMessageView, const TimePoint& timeReceived, Event& event, + std::vector& marketDataMessageList) override { + this->jsonDocumentAllocator.Clear(); + rj::Document document(&this->jsonDocumentAllocator); + document.Parse(textMessageView.data(), textMessageView.size()); + + if (document.IsObject() && document.HasMember("channel")) { + std::string channelId = document["channel"].GetString(); + const auto& data = document["data"]; + + if (channelId == CCAPI_WEBSOCKET_HYPERLIQUID_CHANNEL_BBO) { + std::string symbolId = data["coin"].GetString(); + std::string exchangeSubscriptionId = channelId + ":" + symbolId; + + MarketDataMessage marketDataMessage; + marketDataMessage.tp = TimePoint(std::chrono::milliseconds(std::stoll(data["time"].GetString()))); + marketDataMessage.exchangeSubscriptionId = exchangeSubscriptionId; + marketDataMessage.type = MarketDataMessage::Type::MARKET_DATA_EVENTS_MARKET_DEPTH; + if (this->processedInitialSnapshotByConnectionIdChannelIdSymbolIdMap[wsConnectionPtr->id][channelId][symbolId]) { + marketDataMessage.recapType = MarketDataMessage::RecapType::NONE; + } else { + marketDataMessage.recapType = MarketDataMessage::RecapType::SOLICITED; + } + + const auto& bbo = data["bbo"]; + if (!bbo[0].IsNull()) { + MarketDataMessage::TypeForDataPoint dataPoint; + dataPoint.emplace(MarketDataMessage::DataFieldType::PRICE, UtilString::normalizeDecimalStringView(bbo[0]["px"].GetString())); + dataPoint.emplace(MarketDataMessage::DataFieldType::SIZE, UtilString::normalizeDecimalStringView(bbo[0]["sz"].GetString())); + marketDataMessage.data[MarketDataMessage::DataType::BID].emplace_back(std::move(dataPoint)); + } else { + MarketDataMessage::TypeForDataPoint dataPoint; + dataPoint.emplace(MarketDataMessage::DataFieldType::PRICE, CCAPI_BEST_BID_N_PRICE_EMPTY); + dataPoint.emplace(MarketDataMessage::DataFieldType::SIZE, CCAPI_BEST_BID_N_SIZE_EMPTY); + marketDataMessage.data[MarketDataMessage::DataType::BID].emplace_back(std::move(dataPoint)); + } + + if (!bbo[1].IsNull()) { + MarketDataMessage::TypeForDataPoint dataPoint; + dataPoint.emplace(MarketDataMessage::DataFieldType::PRICE, UtilString::normalizeDecimalStringView(bbo[1]["px"].GetString())); + dataPoint.emplace(MarketDataMessage::DataFieldType::SIZE, UtilString::normalizeDecimalStringView(bbo[1]["sz"].GetString())); + marketDataMessage.data[MarketDataMessage::DataType::ASK].emplace_back(std::move(dataPoint)); + } else { + MarketDataMessage::TypeForDataPoint dataPoint; + dataPoint.emplace(MarketDataMessage::DataFieldType::PRICE, CCAPI_BEST_ASK_N_PRICE_EMPTY); + dataPoint.emplace(MarketDataMessage::DataFieldType::SIZE, CCAPI_BEST_ASK_N_SIZE_EMPTY); + marketDataMessage.data[MarketDataMessage::DataType::ASK].emplace_back(std::move(dataPoint)); + } + marketDataMessageList.emplace_back(std::move(marketDataMessage)); + + } else if (channelId == CCAPI_WEBSOCKET_HYPERLIQUID_CHANNEL_TRADE) { + for (const auto& x : document["data"].GetArray()) { + MarketDataMessage marketDataMessage; + marketDataMessage.type = MarketDataMessage::Type::MARKET_DATA_EVENTS_TRADE; + marketDataMessage.tp = TimePoint(std::chrono::milliseconds(std::stoll(x["time"].GetString()))); + std::string symbolId = x["coin"].GetString(); + std::string exchangeSubscriptionId = channelId + ":" + symbolId; + marketDataMessage.exchangeSubscriptionId = exchangeSubscriptionId; + MarketDataMessage::TypeForDataPoint dataPoint; + dataPoint.emplace(MarketDataMessage::DataFieldType::PRICE, UtilString::normalizeDecimalStringView(x["px"].GetString())); + dataPoint.emplace(MarketDataMessage::DataFieldType::SIZE, UtilString::normalizeDecimalStringView(x["sz"].GetString())); + dataPoint.emplace(MarketDataMessage::DataFieldType::TRADE_ID, x["tid"].GetString()); + dataPoint.emplace(MarketDataMessage::DataFieldType::IS_BUYER_MAKER, std::string_view(x["side"].GetString()) == "A" ? "1" : "0"); + marketDataMessage.data[MarketDataMessage::DataType::TRADE].emplace_back(std::move(dataPoint)); + marketDataMessageList.emplace_back(std::move(marketDataMessage)); + } + } else if (channelId == CCAPI_WEBSOCKET_HYPERLIQUID_CHANNEL_L2_BOOK) { + std::string symbolId = data["coin"].GetString(); + std::string exchangeSubscriptionId = channelId + ":" + symbolId; + + MarketDataMessage marketDataMessage; + marketDataMessage.tp = TimePoint(std::chrono::milliseconds(std::stoll(data["time"].GetString()))); + marketDataMessage.exchangeSubscriptionId = exchangeSubscriptionId; + marketDataMessage.type = MarketDataMessage::Type::MARKET_DATA_EVENTS_MARKET_DEPTH; + if (this->processedInitialSnapshotByConnectionIdChannelIdSymbolIdMap[wsConnectionPtr->id][channelId][symbolId]) { + marketDataMessage.recapType = MarketDataMessage::RecapType::NONE; + } else { + marketDataMessage.recapType = MarketDataMessage::RecapType::SOLICITED; + } + + const auto& levels = data["levels"]; + if (!levels[0].IsNull()) { + for (const auto& x : levels[0].GetArray()) { + MarketDataMessage::TypeForDataPoint dataPoint; + dataPoint.emplace(MarketDataMessage::DataFieldType::PRICE, UtilString::normalizeDecimalStringView(x["px"].GetString())); + dataPoint.emplace(MarketDataMessage::DataFieldType::SIZE, UtilString::normalizeDecimalStringView(x["sz"].GetString())); + marketDataMessage.data[MarketDataMessage::DataType::BID].emplace_back(std::move(dataPoint)); + } + } + + if (!levels[1].IsNull()) { + for (const auto& x : levels[1].GetArray()) { + MarketDataMessage::TypeForDataPoint dataPoint; + dataPoint.emplace(MarketDataMessage::DataFieldType::PRICE, UtilString::normalizeDecimalStringView(x["px"].GetString())); + dataPoint.emplace(MarketDataMessage::DataFieldType::SIZE, UtilString::normalizeDecimalStringView(x["sz"].GetString())); + marketDataMessage.data[MarketDataMessage::DataType::ASK].emplace_back(std::move(dataPoint)); + } + } + marketDataMessageList.emplace_back(std::move(marketDataMessage)); + } + } + } + + void convertRequestForRest(http::request& req, const Request& request, const TimePoint& now, const std::string& symbolId, + const std::map& credential) override { + req.method(http::verb::post); + req.target(this->infoTarget); + req.set(beast::http::field::content_type, "application/json"); + switch (request.getOperation()) { + case Request::Operation::GET_MARKET_DEPTH: { + auto target = this->infoTarget; + rj::Document document; + document.SetObject(); + rj::Document::AllocatorType& allocator = document.GetAllocator(); + document.AddMember("type", rj::Value("l2Book", allocator), allocator); + document.AddMember("coin", rj::Value(symbolId.c_str(), allocator).Move(), allocator); + rj::StringBuffer stringBuffer; + rj::Writer writer(stringBuffer); + document.Accept(writer); + std::string body = stringBuffer.GetString(); + req.body() = body; + req.prepare_payload(); + } break; + case Request::Operation::GET_INSTRUMENTS: { + auto target = this->infoTarget; + rj::Document document; + document.SetObject(); + rj::Document::AllocatorType& allocator = document.GetAllocator(); + document.AddMember("type", rj::Value("meta", allocator), allocator); + rj::StringBuffer stringBuffer; + rj::Writer writer(stringBuffer); + document.Accept(writer); + std::string body = stringBuffer.GetString(); + req.body() = body; + req.prepare_payload(); + } break; + + default: + this->convertRequestForRestCustom(req, request, now, symbolId, credential); + } + } + + void extractInstrumentInfo(Element& element, const rj::Value& x) { + element.insert(CCAPI_INSTRUMENT, x["name"].GetString()); + const auto& szDecimalsStr = x["szDecimals"].GetString(); + int szDecimals = std::stoi(szDecimalsStr); + int pxDecimals = 6 - szDecimals; + if (pxDecimals > 0) { + const auto& priceIncrementStr = "0." + std::string(pxDecimals - 1, '0') + "1"; + element.insert(CCAPI_ORDER_PRICE_INCREMENT, priceIncrementStr); + } else { + element.insert(CCAPI_ORDER_PRICE_INCREMENT, "1"); + } + if (szDecimals > 0) { + const auto& quantityIncrementStr = "0." + std::string(szDecimals - 1, '0') + "1"; + element.insert(CCAPI_ORDER_QUANTITY_INCREMENT, quantityIncrementStr); + element.insert(CCAPI_ORDER_QUANTITY_MIN, quantityIncrementStr); + } else { + element.insert(CCAPI_ORDER_QUANTITY_INCREMENT, "1"); + element.insert(CCAPI_ORDER_QUANTITY_MIN, "1"); + } + } + + void convertTextMessageToMarketDataMessage(const Request& request, boost::beast::string_view textMessageView, const TimePoint& timeReceived, Event& event, + std::vector& marketDataMessageList) override { + this->jsonDocumentAllocator.Clear(); + rj::Document document(&this->jsonDocumentAllocator); + document.Parse(textMessageView.data(), textMessageView.size()); + switch (request.getOperation()) { + case Request::Operation::GET_MARKET_DEPTH: { + MarketDataMessage marketDataMessage; + marketDataMessage.type = MarketDataMessage::Type::MARKET_DATA_EVENTS_MARKET_DEPTH; + const rj::Value& levels = document["levels"]; + marketDataMessage.tp = UtilTime::makeTimePointFromMilliseconds(std::stoll(document["time"].GetString())); + for (const auto& x : levels[0].GetArray()) { + MarketDataMessage::TypeForDataPoint dataPoint; + dataPoint.emplace(MarketDataMessage::DataFieldType::PRICE, x["px"].GetString()); + dataPoint.emplace(MarketDataMessage::DataFieldType::SIZE, x["sz"].GetString()); + marketDataMessage.data[MarketDataMessage::DataType::BID].emplace_back(std::move(dataPoint)); + } + for (const auto& x : levels[1].GetArray()) { + MarketDataMessage::TypeForDataPoint dataPoint; + dataPoint.emplace(MarketDataMessage::DataFieldType::PRICE, x["px"].GetString()); + dataPoint.emplace(MarketDataMessage::DataFieldType::SIZE, x["sz"].GetString()); + marketDataMessage.data[MarketDataMessage::DataType::ASK].emplace_back(std::move(dataPoint)); + } + marketDataMessageList.emplace_back(std::move(marketDataMessage)); + } break; + case Request::Operation::GET_INSTRUMENTS: { + Message message; + message.setTimeReceived(timeReceived); + message.setType(this->requestOperationToMessageTypeMap.at(request.getOperation())); + std::vector elementList; + for (const auto& x : document["universe"].GetArray()) { + Element element; + this->extractInstrumentInfo(element, x); + elementList.push_back(element); + } + message.setElementList(elementList); + message.setCorrelationIdList({request.getCorrelationId()}); + event.addMessages({message}); + } break; + + default: + CCAPI_LOGGER_FATAL(CCAPI_UNSUPPORTED_VALUE); + } + } + + std::string infoTarget; +}; + +} /* namespace ccapi */ +#endif +#endif