From 9721a3d2f64fc85a6caa280fd0c96272b95d3012 Mon Sep 17 00:00:00 2001 From: SebastianBoehler <27767932+SebastianBoehler@users.noreply.github.com> Date: Mon, 11 May 2026 15:59:17 +0200 Subject: [PATCH] feat(clob): centralize fast order execution path --- CMakeLists.txt | 7 + src/clob_client.cpp | 243 --------------------------------- src/clob_order_execution.cpp | 200 +++++++++++++++++++++++++++ src/order_execution.cpp | 78 +++++++++++ src/order_execution.hpp | 38 ++++++ tests/test_order_execution.cpp | 146 ++++++++++++++++++++ 6 files changed, 469 insertions(+), 243 deletions(-) create mode 100644 src/clob_order_execution.cpp create mode 100644 src/order_execution.cpp create mode 100644 src/order_execution.hpp create mode 100644 tests/test_order_execution.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 376595f..ca7aef7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,8 @@ set(POLYMARKET_CLIENT_SOURCES src/websocket_client.cpp src/market_fetcher.cpp src/orderbook.cpp + src/clob_order_execution.cpp + src/order_execution.cpp src/polymarket_events.cpp src/order_signer.cpp src/order_signer_v2.cpp @@ -165,6 +167,11 @@ if(POLYMARKET_CLIENT_BUILD_TESTS) add_executable(test_http_client_transport tests/test_http_client_transport.cpp) target_link_libraries(test_http_client_transport PRIVATE polymarket::client) add_test(NAME test_http_client_transport COMMAND test_http_client_transport) + + add_executable(test_order_execution tests/test_order_execution.cpp) + target_include_directories(test_order_execution PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(test_order_execution PRIVATE polymarket::client) + add_test(NAME test_order_execution COMMAND test_order_execution) endif() # Install library, headers, and dependency targets into a single export set diff --git a/src/clob_client.cpp b/src/clob_client.cpp index 2fe4ea1..8470519 100644 --- a/src/clob_client.cpp +++ b/src/clob_client.cpp @@ -16,7 +16,6 @@ namespace polymarket // Exchange addresses for Polygon mainnet static const std::string EXCHANGE_ADDRESS = "0xE111180000d2663C0091e4f400237545B87B996B"; static const std::string NEG_RISK_EXCHANGE_ADDRESS = "0xe2222d279d744050d28e00520010520000310F59"; - static const std::string ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; // Data API URL for positions static const std::string DATA_API_URL = "https://data-api.polymarket.com"; @@ -679,248 +678,6 @@ namespace polymarket return false; // TODO: Implement DELETE method } - // ============================================================ - // AUTHENTICATED ENDPOINTS (L2 - Trading) - // ============================================================ - - SignedOrder ClobClient::create_order(const CreateOrderParams ¶ms) - { - if (!order_signer_) - { - throw std::runtime_error("Client not authenticated"); - } - - // Use cached neg_risk if provided, otherwise fetch from API - bool is_neg_risk = false; - if (params.neg_risk.has_value()) - { - is_neg_risk = params.neg_risk.value(); - } - else - { - auto neg_risk_info = get_neg_risk(params.token_id); - is_neg_risk = neg_risk_info && neg_risk_info->neg_risk; - } - - std::string exchange_addr = is_neg_risk ? NEG_RISK_EXCHANGE_ADDRESS : EXCHANGE_ADDRESS; - - // Calculate amounts - double maker_amount, taker_amount; - if (params.side == OrderSide::BUY) - { - // BUY: maker pays USDC, receives shares - maker_amount = params.size * params.price; - taker_amount = params.size; - } - else - { - // SELL: maker pays shares, receives USDC - maker_amount = params.size; - taker_amount = params.size * params.price; - } - - OrderData order_data; - order_data.maker = funder_address_.empty() ? order_signer_->address() : funder_address_; - order_data.taker = ZERO_ADDRESS; - order_data.token_id = params.token_id; - order_data.maker_amount = to_wei(maker_amount, 6); - order_data.taker_amount = to_wei(taker_amount, 6); - order_data.side = params.side; - order_data.signer = sig_type_ == SignatureType::POLY_1271 ? order_data.maker : order_signer_->address(); - order_data.expiration = params.expiration; - order_data.metadata = params.metadata; - order_data.builder = params.builder_code; - order_data.signature_type = sig_type_; - - return order_signer_->sign_order(order_data, exchange_addr); - } - - SignedOrder ClobClient::create_market_order(const CreateMarketOrderParams ¶ms) - { - if (!order_signer_) - { - throw std::runtime_error("Client not authenticated"); - } - - // Get current price if not specified - double price = 0.5; - if (params.price) - { - price = *params.price; - } - else - { - auto price_info = get_price(params.token_id, params.side == OrderSide::BUY ? "buy" : "sell"); - if (price_info) - { - price = price_info->price; - } - } - - // Calculate size from amount - double size; - if (params.side == OrderSide::BUY) - { - size = params.amount / price; - } - else - { - size = params.amount; - } - - CreateOrderParams order_params; - order_params.token_id = params.token_id; - order_params.price = price; - order_params.size = size; - order_params.side = params.side; - order_params.metadata = params.metadata; - order_params.builder_code = params.builder_code; - - return create_order(order_params); - } - - OrderResponse ClobClient::post_order(const SignedOrder &order, OrderType order_type) - { - json body; - body["order"] = { - {"salt", std::stoll(order.salt)}, - {"maker", order.maker}, - {"signer", order.signer}, - {"taker", order.taker}, - {"tokenId", order.token_id}, - {"makerAmount", order.maker_amount}, - {"takerAmount", order.taker_amount}, - {"expiration", order.expiration}, - {"side", order.side == 0 ? "BUY" : "SELL"}, - {"signatureType", order.signature_type}, - {"timestamp", order.timestamp}, - {"metadata", order.metadata}, - {"builder", order.builder}, - {"signature", order.signature}}; - body["owner"] = api_creds_ ? api_creds_->api_key : ""; - body["orderType"] = order_type_to_string(order_type); - body["deferExec"] = false; - body["postOnly"] = false; - - std::string body_str = body.dump(); - auto headers = get_l2_headers("POST", "/order", body_str); - auto response = http_.post("/order", body_str, headers); - - return parse_order_response(response.body); - } - - std::vector ClobClient::post_orders(const std::vector &orders) - { - std::vector results; - - if (orders.empty()) - return results; - - json body = json::array(); - for (const auto &entry : orders) - { - json order_json; - order_json["order"] = { - {"salt", std::stoll(entry.order.salt)}, - {"maker", entry.order.maker}, - {"signer", entry.order.signer}, - {"taker", entry.order.taker}, - {"tokenId", entry.order.token_id}, - {"makerAmount", entry.order.maker_amount}, - {"takerAmount", entry.order.taker_amount}, - {"expiration", entry.order.expiration}, - {"side", entry.order.side == 0 ? "BUY" : "SELL"}, - {"signatureType", entry.order.signature_type}, - {"timestamp", entry.order.timestamp}, - {"metadata", entry.order.metadata}, - {"builder", entry.order.builder}, - {"signature", entry.order.signature}}; - order_json["owner"] = api_creds_ ? api_creds_->api_key : ""; - order_json["orderType"] = order_type_to_string(entry.order_type); - order_json["deferExec"] = false; - order_json["postOnly"] = false; - body.push_back(order_json); - } - - std::string body_str = body.dump(); - auto headers = get_l2_headers("POST", "/orders", body_str); - auto response = http_.post("/orders", body_str, headers); - - try - { - auto j = json::parse(response.body); - if (j.is_array()) - { - for (const auto &item : j) - { - results.push_back(parse_order_response(item.dump())); - } - } - } - catch (...) - { - // Single error response - results.push_back(parse_order_response(response.body)); - } - - return results; - } - - OrderResponse ClobClient::create_and_post_order(const CreateOrderParams ¶ms, OrderType order_type) - { - auto signed_order = create_order(params); - return post_order(signed_order, order_type); - } - - OrderResponse ClobClient::create_and_post_market_order(const CreateMarketOrderParams ¶ms, OrderType order_type) - { - auto signed_order = create_market_order(params); - return post_order(signed_order, order_type); - } - - bool ClobClient::cancel_order(const std::string &order_id) - { - json body; - body["orderID"] = order_id; - - std::string body_str = body.dump(); - auto headers = get_l2_headers("DELETE", "/order", body_str); - - // Use POST with body for cancel (API accepts this) - auto response = http_.post("/order", body_str, headers); - return response.ok(); - } - - bool ClobClient::cancel_orders(const std::vector &order_ids) - { - json body = order_ids; - - std::string body_str = body.dump(); - auto headers = get_l2_headers("DELETE", "/orders", body_str); - - auto response = http_.post("/orders", body_str, headers); - return response.ok(); - } - - bool ClobClient::cancel_all() - { - auto headers = get_l2_headers("DELETE", "/cancel-all", ""); - auto response = http_.post("/cancel-all", "{}", headers); - return response.ok(); - } - - bool ClobClient::cancel_market_orders(const std::string &condition_id) - { - json body; - body["market"] = condition_id; - - std::string body_str = body.dump(); - auto headers = get_l2_headers("DELETE", "/cancel-market-orders", body_str); - - auto response = http_.post("/cancel-market-orders", body_str, headers); - return response.ok(); - } - std::optional ClobClient::get_order(const std::string &order_id) { auto headers = get_l2_headers("GET", "/order/" + order_id, ""); diff --git a/src/clob_order_execution.cpp b/src/clob_order_execution.cpp new file mode 100644 index 0000000..530ff45 --- /dev/null +++ b/src/clob_order_execution.cpp @@ -0,0 +1,200 @@ +#include "clob_client.hpp" +#include "order_execution.hpp" +#include "order_signer.hpp" +#include +#include + +using json = nlohmann::json; + +namespace polymarket +{ + namespace + { + constexpr const char *ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + + detail::OrderExecutionContext build_execution_context(const ClobClient &client, + const OrderSigner &signer, + const std::string &funder, + SignatureType signature_type) + { + return { + signer.address(), + funder, + signature_type, + client.get_exchange_address(), + client.get_neg_risk_exchange_address()}; + } + } + + SignedOrder ClobClient::create_order(const CreateOrderParams ¶ms) + { + if (!order_signer_) + { + throw std::runtime_error("Client not authenticated"); + } + + bool is_neg_risk = false; + if (params.neg_risk.has_value()) + { + is_neg_risk = params.neg_risk.value(); + } + else + { + auto neg_risk_info = get_neg_risk(params.token_id); + is_neg_risk = neg_risk_info && neg_risk_info->neg_risk; + } + + const auto context = build_execution_context(*this, *order_signer_, funder_address_, sig_type_); + const auto amounts = detail::calculate_limit_order_amounts(params.side, params.price, params.size); + + OrderData order_data; + order_data.maker = context.maker_address(); + order_data.taker = ZERO_ADDRESS; + order_data.token_id = params.token_id; + order_data.maker_amount = to_wei(amounts.maker, 6); + order_data.taker_amount = to_wei(amounts.taker, 6); + order_data.side = params.side; + order_data.signer = context.signer_for_order(); + order_data.expiration = params.expiration; + order_data.metadata = params.metadata; + order_data.builder = params.builder_code; + order_data.signature_type = sig_type_; + + return order_signer_->sign_order(order_data, context.exchange_for(is_neg_risk)); + } + + SignedOrder ClobClient::create_market_order(const CreateMarketOrderParams ¶ms) + { + if (!order_signer_) + { + throw std::runtime_error("Client not authenticated"); + } + + double price = 0.5; + if (params.price) + { + price = *params.price; + } + else + { + auto price_info = get_price(params.token_id, params.side == OrderSide::BUY ? "buy" : "sell"); + if (price_info) + { + price = price_info->price; + } + } + + CreateOrderParams order_params; + order_params.token_id = params.token_id; + order_params.price = price; + order_params.size = params.side == OrderSide::BUY ? params.amount / price : params.amount; + order_params.side = params.side; + order_params.metadata = params.metadata; + order_params.builder_code = params.builder_code; + + return create_order(order_params); + } + + OrderResponse ClobClient::post_order(const SignedOrder &order, OrderType order_type) + { + const auto body = detail::order_payload_json(order, + api_creds_ ? api_creds_->api_key : "", + order_type_to_string(order_type)); + const std::string body_str = body.dump(); + auto headers = get_l2_headers("POST", "/order", body_str); + auto response = http_.post("/order", body_str, headers); + + return parse_order_response(response.body); + } + + std::vector ClobClient::post_orders(const std::vector &orders) + { + std::vector results; + if (orders.empty()) + { + return results; + } + + const auto body = detail::batch_order_payload_json( + orders, + api_creds_ ? api_creds_->api_key : "", + [this](OrderType type) + { + return order_type_to_string(type); + }); + + const std::string body_str = body.dump(); + auto headers = get_l2_headers("POST", "/orders", body_str); + auto response = http_.post("/orders", body_str, headers); + + try + { + auto response_json = json::parse(response.body); + if (response_json.is_array()) + { + for (const auto &item : response_json) + { + results.push_back(parse_order_response(item.dump())); + } + } + } + catch (...) + { + results.push_back(parse_order_response(response.body)); + } + + return results; + } + + OrderResponse ClobClient::create_and_post_order(const CreateOrderParams ¶ms, OrderType order_type) + { + return post_order(create_order(params), order_type); + } + + OrderResponse ClobClient::create_and_post_market_order(const CreateMarketOrderParams ¶ms, OrderType order_type) + { + return post_order(create_market_order(params), order_type); + } + + bool ClobClient::cancel_order(const std::string &order_id) + { + json body; + body["orderID"] = order_id; + + const std::string body_str = body.dump(); + auto headers = get_l2_headers("DELETE", "/order", body_str); + + auto response = http_.post("/order", body_str, headers); + return response.ok(); + } + + bool ClobClient::cancel_orders(const std::vector &order_ids) + { + const json body = order_ids; + + const std::string body_str = body.dump(); + auto headers = get_l2_headers("DELETE", "/orders", body_str); + + auto response = http_.post("/orders", body_str, headers); + return response.ok(); + } + + bool ClobClient::cancel_all() + { + auto headers = get_l2_headers("DELETE", "/cancel-all", ""); + auto response = http_.post("/cancel-all", "{}", headers); + return response.ok(); + } + + bool ClobClient::cancel_market_orders(const std::string &condition_id) + { + json body; + body["market"] = condition_id; + + const std::string body_str = body.dump(); + auto headers = get_l2_headers("DELETE", "/cancel-market-orders", body_str); + + auto response = http_.post("/cancel-market-orders", body_str, headers); + return response.ok(); + } +} diff --git a/src/order_execution.cpp b/src/order_execution.cpp new file mode 100644 index 0000000..a24c996 --- /dev/null +++ b/src/order_execution.cpp @@ -0,0 +1,78 @@ +#include "order_execution.hpp" + +namespace polymarket::detail +{ + namespace + { + constexpr const char *ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; + } + + std::string OrderExecutionContext::maker_address() const + { + return funder_address.empty() ? signer_address : funder_address; + } + + std::string OrderExecutionContext::signer_for_order() const + { + return signature_type == SignatureType::POLY_1271 ? maker_address() : signer_address; + } + + std::string OrderExecutionContext::exchange_for(bool neg_risk) const + { + return neg_risk ? neg_risk_exchange_address : standard_exchange_address; + } + + OrderAmounts calculate_limit_order_amounts(OrderSide side, double price, double size) + { + if (side == OrderSide::BUY) + { + return {size * price, size}; + } + return {size, size * price}; + } + + nlohmann::json signed_order_json(const SignedOrder &order) + { + return { + {"salt", std::stoll(order.salt)}, + {"maker", order.maker}, + {"signer", order.signer}, + {"taker", order.taker.empty() ? ZERO_ADDRESS : order.taker}, + {"tokenId", order.token_id}, + {"makerAmount", order.maker_amount}, + {"takerAmount", order.taker_amount}, + {"expiration", order.expiration}, + {"side", order.side == 0 ? "BUY" : "SELL"}, + {"signatureType", order.signature_type}, + {"timestamp", order.timestamp}, + {"metadata", order.metadata}, + {"builder", order.builder}, + {"signature", order.signature}}; + } + + nlohmann::json order_payload_json(const SignedOrder &order, + const std::string &owner, + const std::string &order_type, + bool post_only, + bool defer_exec) + { + return { + {"order", signed_order_json(order)}, + {"owner", owner}, + {"orderType", order_type}, + {"deferExec", defer_exec}, + {"postOnly", post_only}}; + } + + nlohmann::json batch_order_payload_json(const std::vector &orders, + const std::string &owner, + const std::function &order_type_to_string) + { + auto body = nlohmann::json::array(); + for (const auto &entry : orders) + { + body.push_back(order_payload_json(entry.order, owner, order_type_to_string(entry.order_type))); + } + return body; + } +} diff --git a/src/order_execution.hpp b/src/order_execution.hpp new file mode 100644 index 0000000..d4c189e --- /dev/null +++ b/src/order_execution.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "clob_client.hpp" +#include +#include + +namespace polymarket::detail +{ + struct OrderExecutionContext + { + std::string signer_address; + std::string funder_address; + SignatureType signature_type{SignatureType::EOA}; + std::string standard_exchange_address; + std::string neg_risk_exchange_address; + + std::string maker_address() const; + std::string signer_for_order() const; + std::string exchange_for(bool neg_risk) const; + }; + + struct OrderAmounts + { + double maker; + double taker; + }; + + OrderAmounts calculate_limit_order_amounts(OrderSide side, double price, double size); + nlohmann::json signed_order_json(const SignedOrder &order); + nlohmann::json order_payload_json(const SignedOrder &order, + const std::string &owner, + const std::string &order_type, + bool post_only = false, + bool defer_exec = false); + nlohmann::json batch_order_payload_json(const std::vector &orders, + const std::string &owner, + const std::function &order_type_to_string); +} diff --git a/tests/test_order_execution.cpp b/tests/test_order_execution.cpp new file mode 100644 index 0000000..d7a7b1a --- /dev/null +++ b/tests/test_order_execution.cpp @@ -0,0 +1,146 @@ +#include "order_execution.hpp" + +#include +#include +#include + +using namespace polymarket; + +namespace +{ + constexpr const char *kSigner = "0x1111111111111111111111111111111111111111"; + constexpr const char *kFunder = "0x2222222222222222222222222222222222222222"; + constexpr const char *kStandardExchange = "0x3333333333333333333333333333333333333333"; + constexpr const char *kNegRiskExchange = "0x4444444444444444444444444444444444444444"; + constexpr const char *kZeroBytes32 = "0x0000000000000000000000000000000000000000000000000000000000000000"; + constexpr const char *kZeroAddress = "0x0000000000000000000000000000000000000000"; + + bool expect_true(const std::string &name, bool value) + { + if (value) + { + return true; + } + std::cerr << "failed: " << name << "\n"; + return false; + } + + bool expect_close(const std::string &name, double actual, double expected) + { + return expect_true(name, std::fabs(actual - expected) < 0.0000001); + } + + bool expect_equal(const std::string &name, const std::string &actual, const std::string &expected) + { + if (actual == expected) + { + return true; + } + std::cerr << name << " mismatch\n" + << " expected: " << expected << "\n" + << " actual: " << actual << "\n"; + return false; + } + + SignedOrder sample_order(const std::string &salt, int side) + { + SignedOrder order; + order.salt = salt; + order.maker = kFunder; + order.signer = kSigner; + order.taker = ""; + order.token_id = "123456789"; + order.maker_amount = "4200000"; + order.taker_amount = "10000000"; + order.expiration = "0"; + order.side = side; + order.signature_type = static_cast(SignatureType::POLY_PROXY); + order.timestamp = "1713398400000"; + order.metadata = kZeroBytes32; + order.builder = kZeroBytes32; + order.signature = "0xabc"; + return order; + } +} + +int main() +{ + const auto buy_amounts = detail::calculate_limit_order_amounts(OrderSide::BUY, 0.42, 10.0); + if (!expect_close("buy maker amount", buy_amounts.maker, 4.2) || + !expect_close("buy taker amount", buy_amounts.taker, 10.0)) + { + return 1; + } + + const auto sell_amounts = detail::calculate_limit_order_amounts(OrderSide::SELL, 0.42, 10.0); + if (!expect_close("sell maker amount", sell_amounts.maker, 10.0) || + !expect_close("sell taker amount", sell_amounts.taker, 4.2)) + { + return 1; + } + + detail::OrderExecutionContext context{ + kSigner, + "", + SignatureType::EOA, + kStandardExchange, + kNegRiskExchange}; + + if (!expect_equal("EOA maker", context.maker_address(), kSigner) || + !expect_equal("EOA signer", context.signer_for_order(), kSigner) || + !expect_equal("standard exchange", context.exchange_for(false), kStandardExchange) || + !expect_equal("neg risk exchange", context.exchange_for(true), kNegRiskExchange)) + { + return 1; + } + + context.funder_address = kFunder; + context.signature_type = SignatureType::POLY_PROXY; + if (!expect_equal("proxy maker", context.maker_address(), kFunder) || + !expect_equal("proxy signer", context.signer_for_order(), kSigner)) + { + return 1; + } + + context.signature_type = SignatureType::POLY_1271; + if (!expect_equal("1271 signer", context.signer_for_order(), kFunder)) + { + return 1; + } + + const auto payload = detail::order_payload_json(sample_order("123456789", 0), "owner-key", "GTC"); + if (!expect_true("payload has order", payload.contains("order")) || + !expect_true("salt is numeric", payload["order"]["salt"].is_number_integer()) || + !expect_equal("taker defaults to zero address", payload["order"]["taker"], kZeroAddress) || + !expect_equal("side string", payload["order"]["side"], "BUY") || + !expect_equal("owner", payload["owner"], "owner-key") || + !expect_equal("order type", payload["orderType"], "GTC") || + !expect_true("post only false", payload["postOnly"] == false) || + !expect_true("defer exec false", payload["deferExec"] == false) || + !expect_true("nonce omitted", !payload["order"].contains("nonce")) || + !expect_true("fee omitted", !payload["order"].contains("feeRateBps"))) + { + return 1; + } + + std::vector batch{ + {sample_order("1", 0), OrderType::GTC}, + {sample_order("2", 1), OrderType::FAK}}; + const auto batch_payload = detail::batch_order_payload_json( + batch, + "owner-key", + [](OrderType type) + { + return type == OrderType::GTC ? "GTC" : "FAK"; + }); + + if (!expect_true("batch is array", batch_payload.is_array()) || + !expect_true("batch size", batch_payload.size() == 2) || + !expect_equal("batch second side", batch_payload[1]["order"]["side"], "SELL") || + !expect_equal("batch second type", batch_payload[1]["orderType"], "FAK")) + { + return 1; + } + + return 0; +}