diff --git a/CMakeLists.txt b/CMakeLists.txt index 1cb944c..68994c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -151,6 +151,7 @@ set(POLYMARKET_CLIENT_SOURCES src/order_execution.cpp src/polymarket_events.cpp src/polymarket_contracts.cpp + src/environment.cpp src/position_calls.cpp src/approval_calls.cpp src/trading_approvals.cpp diff --git a/README.md b/README.md index f5e25fc..0bfa4d3 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,28 @@ std::cout << "avg latency: " << client.get_connection_stats().avg_latency_ms << Order helpers round prices, sizes, and maker/taker amounts to the market's tick size. Leave `tick_size` empty to resolve it from the client's metadata cache. +### Select an environment + +`Environment` holds every endpoint and contract for one deployment. Pass it to +`ClobClient`, or build stream and position configs from it: + +```cpp +#include + +const auto env = polymarket::Environment::preproduction(); // or production() +polymarket::ClobClient client(env, private_key, creds); +polymarket::UserStream stream(polymarket::Config::for_environment(env), creds); +auto position_config = polymarket::PositionClientConfig::for_environment(env); +``` + +`preproduction()` uses separate CLOB, Gamma, Data API, and relayer hosts, but +it is not a testnet. It runs on Polygon mainnet with the production contracts, +RPC, and CLOB WebSocket hosts, so on-chain operations and settled orders use +real funds. API keys are per environment. Copy a preset and override fields to +target a fork or a local server. The string constructors +(`ClobClient(base_url, 137, ...)`) override only the production CLOB host and +accept only chain 137. + ### Route traffic through a proxy or VPN interface ```cpp @@ -268,6 +290,9 @@ Build with `-DPOLYMARKET_CLIENT_BUILD_EXAMPLES=ON` and run from `build/`. | `feed_latency_benchmark` | Compares receive timing across the Polymarket market WS and a Polygon RPC WS | | `polymarket_arb` | Analysis-only scan of complementary YES/NO books (`--15m --symbol btc --fetch-only`) | +Examples that call Polymarket services, and `order_test`, read +`POLYMARKET_ENV` (`production` by default, or `preproduction`). + `polymarket_arb` rejects `--live`: the CLOB batch endpoint processes orders independently, so two complementary FOK orders are not an atomic trade. diff --git a/cmake/PolymarketTests.cmake b/cmake/PolymarketTests.cmake index 1302a22..59fd8fd 100644 --- a/cmake/PolymarketTests.cmake +++ b/cmake/PolymarketTests.cmake @@ -32,6 +32,10 @@ if(POLYMARKET_CLIENT_BUILD_TESTS) target_link_libraries(test_polymarket_contracts PRIVATE polymarket::client) add_test(NAME test_polymarket_contracts COMMAND test_polymarket_contracts) + add_executable(test_environment tests/test_environment.cpp) + target_link_libraries(test_environment PRIVATE polymarket::client) + add_test(NAME test_environment COMMAND test_environment) + add_executable(test_position_client tests/test_position_client.cpp) target_include_directories(test_position_client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) target_link_libraries(test_position_client PRIVATE polymarket::client) @@ -92,6 +96,10 @@ if(POLYMARKET_CLIENT_BUILD_TESTS) target_link_libraries(test_json_rpc_transactions PRIVATE polymarket::client) add_test(NAME test_json_rpc_transactions COMMAND test_json_rpc_transactions) + add_executable(test_clob_environment tests/test_clob_environment.cpp) + target_link_libraries(test_clob_environment PRIVATE polymarket::client) + add_test(NAME test_clob_environment COMMAND test_clob_environment) + add_executable(test_json_rpc_ws_owner_reset tests/test_json_rpc_ws_owner_reset.cpp) target_include_directories(test_json_rpc_ws_owner_reset PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests) @@ -168,6 +176,11 @@ if(POLYMARKET_CLIENT_BUILD_TESTS) add_test(NAME test_oracle_watcher_historical COMMAND test_oracle_watcher_historical) set_tests_properties(test_oracle_watcher_historical PROPERTIES LABELS live) + add_executable(test_preproduction_live tests/test_preproduction_live.cpp) + target_link_libraries(test_preproduction_live PRIVATE polymarket::client) + add_test(NAME test_preproduction_live COMMAND test_preproduction_live) + set_tests_properties(test_preproduction_live PROPERTIES LABELS live) + add_executable(test_arb_sizing tests/test_arb_sizing.cpp) target_include_directories(test_arb_sizing PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) target_link_libraries(test_arb_sizing PRIVATE polymarket::client) diff --git a/examples/approvals_example.cpp b/examples/approvals_example.cpp index 601233a..eb9aa05 100644 --- a/examples/approvals_example.cpp +++ b/examples/approvals_example.cpp @@ -1,3 +1,4 @@ +#include "example_environment.hpp" #include "polymarket/position_client.hpp" #include @@ -17,7 +18,8 @@ // POLYMARKET_PROXY_ADDRESS set, the approvals are set on that Gnosis Safe // through the relayer in one transaction (gasless; needs RELAYER_API_KEY, // optional RELAYER_API_KEY_ADDRESS). Otherwise the EOA sends one transaction -// per missing approval and pays gas in POL. +// per missing approval and pays gas in POL. POLYMARKET_ENV selects the +// relayer and contract preset (default production). namespace { @@ -43,7 +45,7 @@ int main(int argc, char **argv) execute = true; } - PositionClientConfig config; + auto config = PositionClientConfig::for_environment(polymarket_example::selected_environment()); config.private_key = env("PRIVATE_KEY"); config.rpc_url = env("POLYGON_RPC_ENDPOINT"); if (config.private_key.empty() || config.rpc_url.empty()) diff --git a/examples/example_environment.hpp b/examples/example_environment.hpp new file mode 100644 index 0000000..c333588 --- /dev/null +++ b/examples/example_environment.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "polymarket/environment.hpp" +#include + +namespace polymarket_example +{ + // POLYMARKET_ENV: "production" (default) or "preproduction". + inline polymarket::Environment selected_environment() + { + const char *name = std::getenv("POLYMARKET_ENV"); + return polymarket::Environment::from_name(name && *name ? name : "production"); + } +} // namespace polymarket_example diff --git a/examples/position_example.cpp b/examples/position_example.cpp index 76b0453..d563d34 100644 --- a/examples/position_example.cpp +++ b/examples/position_example.cpp @@ -1,3 +1,4 @@ +#include "example_environment.hpp" #include "polymarket/position_client.hpp" #include @@ -17,7 +18,8 @@ // Environment: PRIVATE_KEY and POLYGON_RPC_ENDPOINT are required. With // POLYMARKET_PROXY_ADDRESS set, operations run from that Gnosis Safe through // the relayer (gasless; needs RELAYER_API_KEY, optional RELAYER_API_KEY_ADDRESS). -// Otherwise they are sent from the EOA, which pays gas in POL. +// Otherwise they are sent from the EOA, which pays gas in POL. POLYMARKET_ENV +// selects the Gamma, relayer and contract preset (default production). namespace { @@ -54,7 +56,7 @@ int main(int argc, char **argv) if ((action != "split" && action != "merge" && action != "redeem") || (action == "split" && amount.empty())) return usage(); - PositionClientConfig config; + auto config = PositionClientConfig::for_environment(polymarket_example::selected_environment()); config.private_key = env("PRIVATE_KEY"); config.rpc_url = env("POLYGON_RPC_ENDPOINT"); if (config.private_key.empty() || config.rpc_url.empty()) diff --git a/examples/rest_example.cpp b/examples/rest_example.cpp index 6b702ad..ac1ea8c 100644 --- a/examples/rest_example.cpp +++ b/examples/rest_example.cpp @@ -1,3 +1,4 @@ +#include "example_environment.hpp" #include "polymarket/clob_client.hpp" #include #include @@ -16,6 +17,8 @@ int main() try { http_global_init(); + const auto environment = polymarket_example::selected_environment(); + std::cout << "Environment: " << environment.name << "\n"; // ============================================================ // PUBLIC ENDPOINTS (No authentication required) @@ -23,7 +26,7 @@ int main() std::cout << "=== Public Endpoints ===\n\n"; - ClobClient public_client{"https://clob.polymarket.com", 137}; + ClobClient public_client{environment}; // Get markets auto market_page = public_client.get_markets(); @@ -117,13 +120,7 @@ int main() // Determine signature type based on funder address SignatureType sig_type = funder_address.empty() ? SignatureType::EOA : SignatureType::POLY_PROXY; - ClobClient client{ - "https://clob.polymarket.com", - 137, - private_key, - creds, - sig_type, - funder_address}; + ClobClient client{environment, private_key, creds, sig_type, funder_address}; // Get balance auto balance = client.get_balance_allowance("COLLATERAL"); diff --git a/examples/test_positions.cpp b/examples/test_positions.cpp index 337be53..9f2f4ea 100644 --- a/examples/test_positions.cpp +++ b/examples/test_positions.cpp @@ -1,3 +1,4 @@ +#include "example_environment.hpp" #include "polymarket/clob_client.hpp" #include #include @@ -60,7 +61,7 @@ int main() std::cout << "Testing get_positions for address: " << funder_address << "\n\n"; // Create unauthenticated client (positions API doesn't need auth) - ClobClient client{"https://clob.polymarket.com", 137}; + ClobClient client{polymarket_example::selected_environment()}; // Test get_positions std::cout << "=== All Positions ===\n\n"; diff --git a/examples/user_stream_example.cpp b/examples/user_stream_example.cpp index f802090..c6013a9 100644 --- a/examples/user_stream_example.cpp +++ b/examples/user_stream_example.cpp @@ -1,3 +1,4 @@ +#include "example_environment.hpp" #include "polymarket/clob_client.hpp" #include "polymarket/user_stream.hpp" @@ -24,6 +25,7 @@ int main(int argc, char **argv) try { + const auto environment = polymarket_example::selected_environment(); ApiCredentials credentials; if (has_api_credentials) { @@ -32,11 +34,11 @@ int main(int argc, char **argv) else { // Derive L2 API credentials from the signer (no orders are placed). - ClobClient client("https://clob.polymarket.com", 137, pk_env); + ClobClient client(environment, pk_env); credentials = client.create_or_derive_api_key(); } - UserStream stream(Config{}, credentials); + UserStream stream(Config::for_environment(environment), credentials); stream.on_order([](const UserOrderEvent &order) { std::cout << "[order] " << order.type << ' ' << order.side << ' ' << order.size_matched << '/' << order.original_size diff --git a/examples/ws_example.cpp b/examples/ws_example.cpp index 5150ad7..1637eaa 100644 --- a/examples/ws_example.cpp +++ b/examples/ws_example.cpp @@ -1,3 +1,4 @@ +#include "example_environment.hpp" #include "polymarket/websocket_client.hpp" #include #include @@ -19,7 +20,7 @@ int main() std::atomic connected{false}; WebSocketClient ws; - ws.set_url("wss://ws-subscriptions-clob.polymarket.com/ws/market"); + ws.set_url(polymarket_example::selected_environment().clob_market_ws_url); ws.set_ping_interval_ms(10000); ws.set_auto_reconnect(false); diff --git a/include/polymarket/clob_client.hpp b/include/polymarket/clob_client.hpp index a48a426..0d8c527 100644 --- a/include/polymarket/clob_client.hpp +++ b/include/polymarket/clob_client.hpp @@ -1,6 +1,7 @@ #pragma once #include "polymarket/clob_types.hpp" +#include "polymarket/environment.hpp" #include "polymarket/geoblock.hpp" #include "polymarket/types.hpp" #include "polymarket/http_client.hpp" @@ -43,6 +44,19 @@ namespace polymarket const std::string &funder_address, const HttpClientOptions &http_options); + // The constructors above change only the production CLOB host (chain 137 only). + // These take every host and contract from a validated `environment`. + explicit ClobClient(const Environment &environment, + const std::optional &http_options = std::nullopt); + ClobClient(const Environment &environment, const std::string &private_key, + SignatureType sig_type = SignatureType::EOA, + const std::string &funder_address = "", + const std::optional &http_options = std::nullopt); + ClobClient(const Environment &environment, const std::string &private_key, + const ApiCredentials &creds, SignatureType sig_type = SignatureType::EOA, + const std::string &funder_address = "", + const std::optional &http_options = std::nullopt); + ~ClobClient(); // Disable copy @@ -243,7 +257,8 @@ namespace polymarket HttpClient::ConnectionStats get_connection_stats() const { return http_.get_stats(); } RequestMetrics get_last_request_metrics() const { return http_.get_last_request_metrics(); } - // Get exchange address for the chain + const Environment &environment() const { return environment_; } + std::string get_exchange_address() const; std::string get_neg_risk_exchange_address() const; @@ -266,8 +281,7 @@ namespace polymarket private: HttpClient http_; HttpClient data_http_; - int chain_id_; - std::string base_url_; + Environment environment_; std::string funder_address_; SignatureType sig_type_; @@ -275,6 +289,12 @@ namespace polymarket std::unique_ptr order_signer_; std::unique_ptr api_creds_; + // Every public constructor delegates here; null private_key means public access. + ClobClient(Environment environment, const std::string *private_key, + const ApiCredentials *creds, SignatureType sig_type, + const std::string &funder_address, + const std::optional &http_options); + template struct MetadataCacheEntry { diff --git a/include/polymarket/environment.hpp b/include/polymarket/environment.hpp new file mode 100644 index 0000000..894fcd5 --- /dev/null +++ b/include/polymarket/environment.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "polymarket/polymarket_contracts.hpp" +#include +#include + +namespace polymarket +{ + // Every endpoint and contract for one Polymarket deployment. production() + // and preproduction() match the official py-sdk and ts-sdk presets. Copy a + // preset and override fields to target a fork or a local server. + // + // preproduction() is not a testnet: it uses separate CLOB, Gamma, Data API + // and relayer hosts, but runs on Polygon mainnet (chain 137) with the + // production contracts, RPC and CLOB WebSocket hosts. + struct Environment + { + std::string name; + std::string rpc_url; // Polygon JSON-RPC over HTTP(S) + PolymarketContracts contracts; // contracts.chain_id is the chain ID + + std::string clob_url; + std::string clob_market_ws_url; + std::string clob_user_ws_url; + std::string gamma_url; + std::string data_url; + std::string relayer_url; + std::string rtds_ws_url; + + static Environment production(); + static Environment preproduction(); + // "production" or "preproduction"; throws std::invalid_argument otherwise. + static Environment from_name(std::string_view name); + + // Throws std::invalid_argument naming the first invalid field. + void validate() const; + }; + +} // namespace polymarket diff --git a/include/polymarket/position_client.hpp b/include/polymarket/position_client.hpp index 3f43fe0..e7f9f59 100644 --- a/include/polymarket/position_client.hpp +++ b/include/polymarket/position_client.hpp @@ -16,6 +16,8 @@ namespace polymarket { + struct Environment; + // One contract call inside a transaction. struct ContractCall { @@ -168,6 +170,10 @@ namespace polymarket std::string relayer_api_key_address; // defaults to the signer address long relayer_retry_delay_ms = 2000; // between retried submits int relayer_max_submit_retries = 10; + + // RPC, Gamma, relayer and contracts from `environment`, which is + // validated first. The caller still sets private_key and the wallet. + static PositionClientConfig for_environment(const Environment &environment); }; // Split, merge and redeem binary market positions (CTF and Protocol V2 diff --git a/include/polymarket/types.hpp b/include/polymarket/types.hpp index 873c9eb..50fa07d 100644 --- a/include/polymarket/types.hpp +++ b/include/polymarket/types.hpp @@ -8,6 +8,7 @@ namespace polymarket { + struct Environment; // Price level in orderbook struct PriceLevel @@ -278,6 +279,10 @@ namespace polymarket std::vector crypto_tickers = { "btc", "eth", "xrp", "sol", "doge", "bnb", "ada", "avax", "matic", "link", "dot", "ltc"}; + + // CLOB, Gamma and RTDS endpoints from `environment`, which is validated + // first; every other field keeps its default. + static Config for_environment(const Environment &environment); }; // Monotonic nanoseconds for receipt ages and latency measurements. diff --git a/src/clob_auth_contract_tests.cpp b/src/clob_auth_contract_tests.cpp index 07a950e..8be8193 100644 --- a/src/clob_auth_contract_tests.cpp +++ b/src/clob_auth_contract_tests.cpp @@ -60,46 +60,6 @@ namespace clob_test "installed credentials must generate a valid L2 signature"); } - bool test_clob_constructors_reject_unsupported_chains() - { - constexpr const char *private_key = - "0x0000000000000000000000000000000000000000000000000000000000000001"; - constexpr const char *funder = "0x1111111111111111111111111111111111111111"; - LocalServer server; - HttpClientOptions options; - ApiCredentials credentials{"key", "c2VjcmV0", "pass"}; - const auto rejects = [](auto construct) - { - try - { - construct(); - } - catch (const std::invalid_argument &) - { - return true; - } - catch (...) - { - } - return false; - }; - - const bool all_rejected = - rejects([&] { ClobClient client(server.url(), 1); }) && - rejects([&] { ClobClient client(server.url(), 1, options); }) && - rejects([&] { ClobClient client(server.url(), 1, private_key); }) && - rejects([&] { ClobClient client(server.url(), 1, private_key, - SignatureType::POLY_PROXY, funder, options); }) && - rejects([&] { ClobClient client(server.url(), 1, private_key, credentials); }) && - rejects([&] { ClobClient client(server.url(), 1, private_key, credentials, - SignatureType::POLY_PROXY, funder, options); }); - ClobClient polygon(server.url(), 137); - ClobClient amoy(server.url(), 80002); - return check(all_rejected, "every CLOB constructor must reject unsupported chains") && - check(polygon.get_address().empty() && amoy.get_address().empty(), - "Polygon mainnet and Amoy chains must remain supported"); - } - bool test_clob_constructors_require_non_eoa_funder() { constexpr const char *key = diff --git a/src/clob_client.cpp b/src/clob_client.cpp index dc291f2..a48bfb3 100644 --- a/src/clob_client.cpp +++ b/src/clob_client.cpp @@ -8,8 +8,10 @@ #include #include #include +#include #include #include +#include using json = nlohmann::json; @@ -17,19 +19,23 @@ namespace polymarket { using detail::percent_encode_query_value; - static const PolymarketContracts &mainnet_contracts() + static Environment legacy_environment(const std::string &base_url, int chain_id) { - static const PolymarketContracts contracts = PolymarketContracts::polygon_mainnet(); - return contracts; + if (chain_id != 137) + throw std::invalid_argument("unsupported CLOB chain ID " + std::to_string(chain_id) + + "; use an Environment for other chains"); + Environment environment = Environment::production(); + environment.clob_url = base_url; + return environment; } - static constexpr const char *DATA_API_URL = "https://data-api.polymarket.com"; - - static int validated_chain_id(int chain_id) + static Environment validated_environment(const Environment &environment) { - if (chain_id != 137 && chain_id != 80002) - throw std::invalid_argument("unsupported CLOB chain ID"); - return chain_id; + environment.validate(); + if (environment.contracts.chain_id > static_cast(std::numeric_limits::max())) + throw std::invalid_argument( + "Environment.contracts.chain_id exceeds the CLOB signer range"); + return environment; } static std::string validated_funder(SignatureType signature_type, @@ -49,23 +55,22 @@ namespace polymarket throw std::invalid_argument("unsupported signature type"); } - static void configure_default_transports(HttpClient &clob, HttpClient &data, - const std::string &clob_url) - { - clob.set_base_url(clob_url); - data.set_base_url(DATA_API_URL); - clob.set_timeout_ms(10000); - data.set_timeout_ms(10000); - } - static void configure_transports(HttpClient &clob, HttpClient &data, - const std::string &clob_url, - const HttpClientOptions &options) + const Environment &environment, + const std::optional &options) { - clob.configure(options); - data.configure(options); - clob.set_base_url(clob_url); - data.set_base_url(DATA_API_URL); + if (options) + { + clob.configure(*options); + data.configure(*options); + } + else + { + clob.set_timeout_ms(10000); + data.set_timeout_ms(10000); + } + clob.set_base_url(environment.clob_url); + data.set_base_url(environment.data_url); } static ClobMarketPage fetch_market_page( @@ -87,84 +92,103 @@ namespace polymarket } } + ClobClient::ClobClient(Environment environment, const std::string *private_key, + const ApiCredentials *creds, SignatureType sig_type, + const std::string &funder_address, + const std::optional &http_options) + : environment_(std::move(environment)), + funder_address_(private_key ? validated_funder(sig_type, funder_address) : std::string()), + sig_type_(private_key ? sig_type : SignatureType::EOA) + { + configure_transports(http_, data_http_, environment_, http_options); + if (!private_key) return; + order_signer_ = std::make_unique( + *private_key, static_cast(environment_.contracts.chain_id)); + if (!creds) return; + detail::validate_api_credentials(*creds); + api_creds_ = std::make_unique(*creds); + } + ClobClient::ClobClient(const std::string &base_url, int chain_id) - : chain_id_(validated_chain_id(chain_id)), base_url_(base_url), sig_type_(SignatureType::EOA) + : ClobClient(legacy_environment(base_url, chain_id), nullptr, nullptr, SignatureType::EOA, + {}, std::nullopt) { - configure_default_transports(http_, data_http_, base_url); } - ClobClient::ClobClient(const std::string &base_url, int chain_id, const HttpClientOptions &http_options) - : chain_id_(validated_chain_id(chain_id)), base_url_(base_url), sig_type_(SignatureType::EOA) + ClobClient::ClobClient(const std::string &base_url, int chain_id, + const HttpClientOptions &http_options) + : ClobClient(legacy_environment(base_url, chain_id), nullptr, nullptr, SignatureType::EOA, + {}, http_options) { - configure_transports(http_, data_http_, base_url, http_options); } ClobClient::ClobClient(const std::string &base_url, int chain_id, const std::string &private_key, SignatureType sig_type, const std::string &funder_address) - : chain_id_(validated_chain_id(chain_id)), base_url_(base_url), - funder_address_(validated_funder(sig_type, funder_address)), - sig_type_(sig_type) + : ClobClient(legacy_environment(base_url, chain_id), &private_key, nullptr, sig_type, + funder_address, std::nullopt) { - configure_default_transports(http_, data_http_, base_url); - order_signer_ = std::make_unique(private_key, chain_id); } ClobClient::ClobClient(const std::string &base_url, int chain_id, const std::string &private_key, SignatureType sig_type, - const std::string &funder_address, - const HttpClientOptions &http_options) - : chain_id_(validated_chain_id(chain_id)), base_url_(base_url), - funder_address_(validated_funder(sig_type, funder_address)), - sig_type_(sig_type) + const std::string &funder_address, const HttpClientOptions &http_options) + : ClobClient(legacy_environment(base_url, chain_id), &private_key, nullptr, sig_type, + funder_address, http_options) { - configure_transports(http_, data_http_, base_url, http_options); - order_signer_ = std::make_unique(private_key, chain_id); } ClobClient::ClobClient(const std::string &base_url, int chain_id, - const std::string &private_key, - const ApiCredentials &creds, - SignatureType sig_type, - const std::string &funder_address) - : chain_id_(validated_chain_id(chain_id)), base_url_(base_url), - funder_address_(validated_funder(sig_type, funder_address)), - sig_type_(sig_type) + const std::string &private_key, const ApiCredentials &creds, + SignatureType sig_type, const std::string &funder_address) + : ClobClient(legacy_environment(base_url, chain_id), &private_key, &creds, sig_type, + funder_address, std::nullopt) { - configure_default_transports(http_, data_http_, base_url); - - order_signer_ = std::make_unique(private_key, chain_id); - detail::validate_api_credentials(creds); - api_creds_ = std::make_unique(creds); } ClobClient::ClobClient(const std::string &base_url, int chain_id, - const std::string &private_key, - const ApiCredentials &creds, - SignatureType sig_type, - const std::string &funder_address, + const std::string &private_key, const ApiCredentials &creds, + SignatureType sig_type, const std::string &funder_address, const HttpClientOptions &http_options) - : chain_id_(validated_chain_id(chain_id)), base_url_(base_url), - funder_address_(validated_funder(sig_type, funder_address)), - sig_type_(sig_type) + : ClobClient(legacy_environment(base_url, chain_id), &private_key, &creds, sig_type, + funder_address, http_options) + { + } + + ClobClient::ClobClient(const Environment &environment, + const std::optional &http_options) + : ClobClient(validated_environment(environment), nullptr, nullptr, SignatureType::EOA, {}, + http_options) { - configure_transports(http_, data_http_, base_url, http_options); + } - order_signer_ = std::make_unique(private_key, chain_id); - detail::validate_api_credentials(creds); - api_creds_ = std::make_unique(creds); + ClobClient::ClobClient(const Environment &environment, const std::string &private_key, + SignatureType sig_type, const std::string &funder_address, + const std::optional &http_options) + : ClobClient(validated_environment(environment), &private_key, nullptr, sig_type, + funder_address, http_options) + { + } + + ClobClient::ClobClient(const Environment &environment, const std::string &private_key, + const ApiCredentials &creds, SignatureType sig_type, + const std::string &funder_address, + const std::optional &http_options) + : ClobClient(validated_environment(environment), &private_key, &creds, sig_type, + funder_address, http_options) + { } ClobClient::~ClobClient() = default; std::string ClobClient::get_exchange_address() const { - return mainnet_contracts().standard_exchange; + return environment_.contracts.standard_exchange; } std::string ClobClient::get_neg_risk_exchange_address() const { - return mainnet_contracts().neg_risk_exchange; + return environment_.contracts.neg_risk_exchange; } bool ClobClient::warm_connection() diff --git a/src/clob_client_test.cpp b/src/clob_client_test.cpp index 0710bf5..fa3913e 100644 --- a/src/clob_client_test.cpp +++ b/src/clob_client_test.cpp @@ -253,7 +253,6 @@ int main() test_metadata_cache_invalidation_refreshes_values() && test_signer_only_l1_bootstrap_installs_credentials() && test_credentials_validation() && - test_clob_constructors_reject_unsupported_chains() && test_clob_constructors_require_non_eoa_funder() && test_clob_warm_connection_uses_time_only() && test_order_result_schema_failures() && test_order_post_response_contracts() && test_cancellation_response_contracts() && diff --git a/src/clob_client_test_fixture.hpp b/src/clob_client_test_fixture.hpp index 9df80a8..24a6fdb 100644 --- a/src/clob_client_test_fixture.hpp +++ b/src/clob_client_test_fixture.hpp @@ -224,7 +224,6 @@ namespace clob_test bool test_metadata_cache_coalesces_concurrent_cold_misses(); bool test_metadata_cache_invalidation_refreshes_values(); bool test_signer_only_l1_bootstrap_installs_credentials(); - bool test_clob_constructors_reject_unsupported_chains(); bool test_clob_constructors_require_non_eoa_funder(); bool test_clob_warm_connection_uses_time_only(); bool test_order_result_schema_failures(); diff --git a/src/environment.cpp b/src/environment.cpp new file mode 100644 index 0000000..a545d43 --- /dev/null +++ b/src/environment.cpp @@ -0,0 +1,108 @@ +#include "polymarket/environment.hpp" +#include "polymarket/position_client.hpp" +#include "polymarket/types.hpp" +#include +#include +#include + +namespace polymarket +{ + namespace + { + bool has_scheme_and_host(const std::string &url, std::string_view scheme) + { + return url.size() > scheme.size() && url.compare(0, scheme.size(), scheme) == 0; + } + + void require_url(const char *field, const std::string &url, std::string_view secure_scheme, + std::string_view plain_scheme) + { + if (has_scheme_and_host(url, secure_scheme) || has_scheme_and_host(url, plain_scheme)) + return; + throw std::invalid_argument(std::string("Environment.") + field + " must be a " + + std::string(secure_scheme) + " or " + + std::string(plain_scheme) + " URL"); + } + } // namespace + + Environment Environment::production() + { + Environment env; + env.name = "production"; + env.rpc_url = "https://polygon.drpc.org"; + env.contracts = PolymarketContracts::polygon_mainnet(); + env.clob_url = "https://clob.polymarket.com"; + env.clob_market_ws_url = "wss://ws-subscriptions-clob.polymarket.com/ws/market"; + env.clob_user_ws_url = "wss://ws-subscriptions-clob.polymarket.com/ws/user"; + env.gamma_url = "https://gamma-api.polymarket.com"; + env.data_url = "https://data-api.polymarket.com"; + env.relayer_url = "https://relayer-v2.polymarket.com"; + env.rtds_ws_url = "wss://ws-live-data.polymarket.com"; + return env; + } + + Environment Environment::preproduction() + { + Environment env = production(); + env.name = "preproduction"; + env.clob_url = "https://clob-preprod-int-v2.polymarket.com"; + env.gamma_url = "https://gamma-api-preprod-int.polymarket.com"; + env.data_url = "https://data-api-preprod-int.polymarket.com"; + env.relayer_url = "https://relayer-v2-preprod-int.polymarket.com"; + return env; + } + + Environment Environment::from_name(std::string_view name) + { + if (name == "production") return production(); + if (name == "preproduction") return preproduction(); + throw std::invalid_argument("unknown Polymarket environment \"" + std::string(name) + + "\"; expected production or preproduction"); + } + + void Environment::validate() const + { + if (name.empty()) throw std::invalid_argument("Environment.name must be non-empty"); + + const std::vector> rest_urls = { + {"rpc_url", &rpc_url}, {"clob_url", &clob_url}, {"gamma_url", &gamma_url}, + {"data_url", &data_url}, {"relayer_url", &relayer_url}, + }; + for (const auto &[field, url] : rest_urls) + require_url(field, *url, "https://", "http://"); + + const std::vector> ws_urls = { + {"clob_market_ws_url", &clob_market_ws_url}, + {"clob_user_ws_url", &clob_user_ws_url}, + {"rtds_ws_url", &rtds_ws_url}, + }; + for (const auto &[field, url] : ws_urls) + require_url(field, *url, "wss://", "ws://"); + + contracts.validate(); + } + + Config Config::for_environment(const Environment &environment) + { + environment.validate(); + Config config; + config.clob_rest_url = environment.clob_url; + config.clob_ws_url = environment.clob_market_ws_url; + config.clob_user_ws_url = environment.clob_user_ws_url; + config.gamma_api_url = environment.gamma_url; + config.rtds_ws_url = environment.rtds_ws_url; + return config; + } + + PositionClientConfig PositionClientConfig::for_environment(const Environment &environment) + { + environment.validate(); + PositionClientConfig config; + config.rpc_url = environment.rpc_url; + config.gamma_api_url = environment.gamma_url; + config.relayer_url = environment.relayer_url; + config.contracts = environment.contracts; + return config; + } + +} // namespace polymarket diff --git a/src/order_test.cpp b/src/order_test.cpp index 34cdf5b..a214e17 100644 --- a/src/order_test.cpp +++ b/src/order_test.cpp @@ -13,6 +13,7 @@ #include "polymarket/order_signer.hpp" #include "order_test_live.hpp" +#include "../examples/example_environment.hpp" #include "polymarket/http_client.hpp" #include #include @@ -31,7 +32,8 @@ void print_usage() << " FUNDER_ADDRESS - Address holding funds (for proxy wallets)\n" << " API_KEY - Polymarket API key\n" << " API_SECRET - Polymarket API secret\n" - << " API_PASSPHRASE - Polymarket API passphrase\n\n" + << " API_PASSPHRASE - Polymarket API passphrase\n" + << " POLYMARKET_ENV - production (default) or preproduction\n\n" << "Options:\n" << " --live - Actually place orders (default: dry-run)\n" << " --help - Show this help\n" @@ -81,9 +83,12 @@ int main(int argc, char *argv[]) try { + const auto environment = polymarket_example::selected_environment(); + std::cout << "Environment: " << environment.name << " (" << environment.clob_url << ")\n\n"; + // Initialize signer std::cout << "[1] Initializing order signer...\n"; - OrderSigner signer(private_key, 137); // Polygon mainnet + OrderSigner signer(private_key, static_cast(environment.contracts.chain_id)); std::cout << " Derived address: " << signer.address() << "\n"; @@ -109,7 +114,7 @@ int main(int argc, char *argv[]) order.signature_type = SignatureType::EOA; // Use neg_risk exchange for crypto markets - auto signed_order = signer.sign_order(order, order_test::NEG_RISK_CTF_EXCHANGE); + auto signed_order = signer.sign_order(order, environment.contracts.neg_risk_exchange); // Also test with FIXED parameters for comparison with TypeScript std::cout << "\n[2b] Testing with FIXED params for TypeScript comparison...\n"; @@ -128,7 +133,8 @@ int main(int argc, char *argv[]) fixed_order.timestamp = "1713398400000"; std::string fixed_salt = "123456789"; - auto signed_order_fixed = signer.sign_order_with_salt(fixed_order, order_test::NEG_RISK_CTF_EXCHANGE, fixed_salt); + auto signed_order_fixed = signer.sign_order_with_salt( + fixed_order, environment.contracts.neg_risk_exchange, fixed_salt); std::cout << " Fixed salt: " << fixed_salt << "\n"; std::cout << " C++ Signature: " << signed_order_fixed.signature << "\n"; std::cout << " Expected (official V2 SDK): 0x172933dc26efdf531dc959a95743b5c13147c5027a1eaa172701f1e599a130851f8126ce1c4fa043e360e4ee30211f49151d6f192a32b1cbad2123463e3d45641c\n"; @@ -170,7 +176,7 @@ int main(int argc, char *argv[]) http_global_init(); HttpClient http; - http.set_base_url(order_test::CLOB_API); + http.set_base_url(environment.clob_url); http.set_timeout_ms(5000); auto response = http.get("/"); @@ -222,7 +228,7 @@ int main(int argc, char *argv[]) std::cout << " POLY_ADDRESS: " << headers.poly_address << "\n"; HttpClient auth_http; - auth_http.set_base_url(order_test::CLOB_API); + auth_http.set_base_url(environment.clob_url); auth_http.set_timeout_ms(10000); std::map auth_headers; @@ -247,7 +253,8 @@ int main(int argc, char *argv[]) if (live_mode) { - if (!order_test::run_live_order(private_key, funder_address, creds, have_creds, signer)) + if (!order_test::run_live_order(environment, private_key, funder_address, creds, + have_creds, signer)) { http_global_cleanup(); return 1; diff --git a/src/order_test_live.cpp b/src/order_test_live.cpp index f1678cc..56fbfa5 100644 --- a/src/order_test_live.cpp +++ b/src/order_test_live.cpp @@ -27,12 +27,12 @@ namespace polymarket::order_test bool neg_risk = false; }; - LiveMarketData find_live_market() + LiveMarketData find_live_market(const Environment &environment) { LiveMarketData live_market; const auto now_ts = static_cast(std::time(nullptr)); constexpr uint64_t min_time_left = 2 * 60; - ClobClient market_data_client(CLOB_API, 137); + ClobClient market_data_client(environment); market_data_client.set_user_agent("polymarket-cpp-client/order-test"); std::vector> candidates; @@ -57,7 +57,7 @@ namespace polymarket::order_test const uint64_t time_left = expiry_ts - now_ts; HttpClient gamma_http; - gamma_http.set_base_url("https://gamma-api.polymarket.com"); + gamma_http.set_base_url(environment.gamma_url); gamma_http.set_timeout_ms(10000); gamma_http.set_user_agent("polymarket-cpp-client/order-test"); @@ -128,16 +128,14 @@ namespace polymarket::order_test } } - bool run_live_order(const std::string &private_key, - const std::string &funder_address, - const ApiCredentials &credentials, - bool have_credentials, - const OrderSigner &signer) + bool run_live_order(const Environment &environment, const std::string &private_key, + const std::string &funder_address, const ApiCredentials &credentials, + bool have_credentials, const OrderSigner &signer) { std::cout << "\n[7] LIVE MODE - Placing $1 test order on BTC market...\n"; std::cout << " Fetching nearest active BTC 15m market...\n"; - const LiveMarketData live_market = find_live_market(); + const LiveMarketData live_market = find_live_market(environment); if (live_market.token_id.empty() || live_market.best_ask <= 0.0 || live_market.tick_size.empty()) { std::cerr << " Could not find active BTC 15m market with complete trading metadata\n"; @@ -146,7 +144,7 @@ namespace polymarket::order_test std::cout << " YES token: " << live_market.token_id.substr(0, 30) << "...\n"; - const std::string exchange_address = live_market.neg_risk ? NEG_RISK_CTF_EXCHANGE : CTF_EXCHANGE; + const std::string &exchange_address = environment.contracts.exchange(live_market.neg_risk); std::cout << " Exchange: " << exchange_address << "\n"; if (!have_credentials) @@ -159,7 +157,8 @@ namespace polymarket::order_test const SignatureType live_signature_type = (funder_address != signer.address()) ? SignatureType::POLY_GNOSIS_SAFE : SignatureType::EOA; - ClobClient order_client(CLOB_API, 137, private_key, credentials, live_signature_type, funder_address); + ClobClient order_client(environment, private_key, credentials, live_signature_type, + funder_address); CreateMarketOrderParams market_order; market_order.token_id = live_market.token_id; diff --git a/src/order_test_live.hpp b/src/order_test_live.hpp index 9b29ea9..307dc96 100644 --- a/src/order_test_live.hpp +++ b/src/order_test_live.hpp @@ -1,18 +1,13 @@ #pragma once +#include "polymarket/environment.hpp" #include "polymarket/order_signer.hpp" #include namespace polymarket::order_test { - inline constexpr char CLOB_API[] = "https://clob.polymarket.com"; - inline constexpr char NEG_RISK_CTF_EXCHANGE[] = "0xe2222d279d744050d28e00520010520000310F59"; - inline constexpr char CTF_EXCHANGE[] = "0xE111180000d2663C0091e4f400237545B87B996B"; - - bool run_live_order(const std::string &private_key, - const std::string &funder_address, - const ApiCredentials &credentials, - bool have_credentials, - const OrderSigner &signer); + bool run_live_order(const Environment &environment, const std::string &private_key, + const std::string &funder_address, const ApiCredentials &credentials, + bool have_credentials, const OrderSigner &signer); } diff --git a/tests/package_consumer/main.cpp b/tests/package_consumer/main.cpp index 168fdf4..ed0efb6 100644 --- a/tests/package_consumer/main.cpp +++ b/tests/package_consumer/main.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -22,7 +23,9 @@ int main() polymarket::HttpClient client; const polymarket::Config config; const polymarket::PositionClientConfig position_config; - if (position_config.contracts.chain_id != 137 || + const auto environment = polymarket::Environment::preproduction(); + environment.validate(); + if (position_config.contracts.chain_id != 137 || environment.contracts.chain_id != 137 || polymarket::evm_function_selector("transfer(address,uint256)") != "0xa9059cbb") return 1; // The installed stream must link and reject absent credentials without connecting. diff --git a/tests/test_clob_environment.cpp b/tests/test_clob_environment.cpp new file mode 100644 index 0000000..3d1e6f0 --- /dev/null +++ b/tests/test_clob_environment.cpp @@ -0,0 +1,147 @@ +#include "../src/clob_client_test_fixture.hpp" + +namespace clob_test +{ + namespace + { + constexpr const char *private_key = + "0x0000000000000000000000000000000000000000000000000000000000000001"; + constexpr const char *wallet = "0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf"; + + Environment local_environment(const LocalServer &clob, const LocalServer &data) + { + auto environment = Environment::production(); + environment.name = "local"; + environment.clob_url = clob.url(); + environment.data_url = data.url(); + environment.contracts.standard_exchange = "0x1111111111111111111111111111111111111111"; + environment.contracts.neg_risk_exchange = "0x2222222222222222222222222222222222222222"; + return environment; + } + + template bool rejects(Construct construct) + { + try + { + construct(); + } + catch (const std::invalid_argument &) + { + return true; + } + catch (...) + { + } + return false; + } + } // namespace + + bool test_clob_environment_routes_hosts_and_contracts() + { + LocalServer clob; + LocalServer data; + const auto environment = local_environment(clob, data); + ClobClient client(environment, private_key, ApiCredentials{"key", "c2VjcmV0", "pass"}); + clob.enqueue("1722510000"); + const bool warmed = client.warm_connection(); + data.enqueue("[]"); + const auto positions = client.get_positions(wallet); + const auto clob_requests = clob.requests(); + const auto data_requests = data.requests(); + + return check(warmed && clob_requests.size() == 1 && clob_requests[0].target == "/time", + "CLOB requests must use Environment.clob_url") && + check(positions.empty() && data_requests.size() == 1 && + data_requests[0].target.rfind("/positions?", 0) == 0, + "Data API requests must use Environment.data_url") && + check(client.get_exchange_address() == environment.contracts.standard_exchange && + client.get_neg_risk_exchange_address() == + environment.contracts.neg_risk_exchange, + "order exchanges must come from Environment.contracts") && + check(client.environment().name == "local" && client.get_address() == wallet, + "client must keep the environment and signer"); + } + + bool test_clob_legacy_constructors_use_production_environment() + { + LocalServer server; + ClobClient client(server.url(), 137); + const ClobClient defaults; + const auto production = Environment::production(); + return check(client.environment().clob_url == server.url() && + client.environment().data_url == production.data_url && + client.get_exchange_address() == production.contracts.standard_exchange && + client.get_neg_risk_exchange_address() == + production.contracts.neg_risk_exchange, + "legacy constructors must override only the CLOB host") && + check(defaults.environment().clob_url == production.clob_url, + "the default CLOB host must match the production preset"); + } + + bool test_clob_environment_constructors_validate() + { + auto bad_url = Environment::production(); + bad_url.data_url = "data-api.polymarket.com"; + auto bad_contract = Environment::production(); + bad_contract.contracts.standard_exchange = "0x12"; + auto wide_chain = Environment::production(); + wide_chain.contracts.chain_id = 1ULL << 40; + ApiCredentials credentials{"key", "c2VjcmV0", "pass"}; + + return check(rejects([&] { ClobClient client(bad_url); }) && + rejects([&] { ClobClient client(bad_contract, private_key); }) && + rejects([&] { ClobClient client(wide_chain, private_key, credentials); }), + "Environment constructors must reject invalid environments") && + check(rejects( + [&] + { + ClobClient client(Environment::production(), private_key, + SignatureType::POLY_GNOSIS_SAFE); + }), + "Environment constructors must still require a non-EOA funder"); + } + + // Legacy constructors sign against the production contracts, so they accept + // only chain 137; Amoy (80002) has no contract preset. + bool test_clob_constructors_reject_unsupported_chains() + { + constexpr const char *funder = "0x1111111111111111111111111111111111111111"; + LocalServer server; + HttpClientOptions options; + ApiCredentials credentials{"key", "c2VjcmV0", "pass"}; + + const bool all_rejected = + rejects([&] { ClobClient client(server.url(), 1); }) && + rejects([&] { ClobClient client(server.url(), 80002); }) && + rejects([&] { ClobClient client(server.url(), 1, options); }) && + rejects([&] { ClobClient client(server.url(), 1, private_key); }) && + rejects( + [&] + { + ClobClient client(server.url(), 1, private_key, SignatureType::POLY_PROXY, + funder, options); + }) && + rejects([&] { ClobClient client(server.url(), 1, private_key, credentials); }) && + rejects( + [&] + { + ClobClient client(server.url(), 1, private_key, credentials, + SignatureType::POLY_PROXY, funder, options); + }); + ClobClient polygon(server.url(), 137); + return check(all_rejected, "every CLOB constructor must reject unsupported chains") && + check(polygon.get_address().empty(), "Polygon mainnet must remain supported"); + } +} // namespace clob_test + +int main() +{ + using namespace clob_test; + polymarket::http_global_init(); + const bool ok = test_clob_environment_routes_hosts_and_contracts() && + test_clob_legacy_constructors_use_production_environment() && + test_clob_environment_constructors_validate() && + test_clob_constructors_reject_unsupported_chains(); + polymarket::http_global_cleanup(); + return ok ? 0 : 1; +} diff --git a/tests/test_environment.cpp b/tests/test_environment.cpp new file mode 100644 index 0000000..28b6eb2 --- /dev/null +++ b/tests/test_environment.cpp @@ -0,0 +1,205 @@ +#include "check_support.hpp" +#include "polymarket/environment.hpp" +#include "polymarket/position_client.hpp" +#include "polymarket/types.hpp" +#include +#include + +namespace +{ + using check_support::check; + using check_support::expect_equal; + using polymarket::Config; + using polymarket::Environment; + using polymarket::PositionClientConfig; + + void expect_invalid(const std::string &name, const std::function &action) + { + check_support::expect_throws(name, action); + } + + // Values from Polymarket/ts-sdk packages/client/src/environments.ts and + // Polymarket/py-sdk src/polymarket/environments.py. + void test_production_preset() + { + const auto env = Environment::production(); + expect_equal("production name", env.name, "production"); + expect_equal("production rpc", env.rpc_url, "https://polygon.drpc.org"); + check(env.contracts.chain_id == 137, "production chain_id must be 137"); + expect_equal("production exchange", env.contracts.standard_exchange, + "0xE111180000d2663C0091e4f400237545B87B996B"); + expect_equal("production clob", env.clob_url, "https://clob.polymarket.com"); + expect_equal("production market ws", env.clob_market_ws_url, + "wss://ws-subscriptions-clob.polymarket.com/ws/market"); + expect_equal("production user ws", env.clob_user_ws_url, + "wss://ws-subscriptions-clob.polymarket.com/ws/user"); + expect_equal("production gamma", env.gamma_url, "https://gamma-api.polymarket.com"); + expect_equal("production data", env.data_url, "https://data-api.polymarket.com"); + expect_equal("production relayer", env.relayer_url, "https://relayer-v2.polymarket.com"); + expect_equal("production rtds", env.rtds_ws_url, "wss://ws-live-data.polymarket.com"); + env.validate(); + } + + // Preproduction forks production and changes only four REST hosts. + void test_preproduction_preset() + { + const auto prod = Environment::production(); + const auto env = Environment::preproduction(); + expect_equal("preproduction name", env.name, "preproduction"); + expect_equal("preproduction clob", env.clob_url, + "https://clob-preprod-int-v2.polymarket.com"); + expect_equal("preproduction gamma", env.gamma_url, + "https://gamma-api-preprod-int.polymarket.com"); + expect_equal("preproduction data", env.data_url, + "https://data-api-preprod-int.polymarket.com"); + expect_equal("preproduction relayer", env.relayer_url, + "https://relayer-v2-preprod-int.polymarket.com"); + + auto restored = env; + restored.name = prod.name; + restored.clob_url = prod.clob_url; + restored.gamma_url = prod.gamma_url; + restored.data_url = prod.data_url; + restored.relayer_url = prod.relayer_url; + check(restored.rpc_url == prod.rpc_url && + restored.contracts.chain_id == prod.contracts.chain_id && + restored.contracts.standard_exchange == prod.contracts.standard_exchange && + restored.contracts.neg_risk_exchange == prod.contracts.neg_risk_exchange && + restored.contracts.collateral_token == prod.contracts.collateral_token && + restored.clob_market_ws_url == prod.clob_market_ws_url && + restored.clob_user_ws_url == prod.clob_user_ws_url && + restored.rtds_ws_url == prod.rtds_ws_url, + "preproduction must match production outside the four REST hosts"); + env.validate(); + } + + void test_from_name() + { + expect_equal("from_name production", Environment::from_name("production").clob_url, + Environment::production().clob_url); + expect_equal("from_name preproduction", Environment::from_name("preproduction").clob_url, + Environment::preproduction().clob_url); + expect_invalid("from_name empty", [] { Environment::from_name(""); }); + expect_invalid("from_name alias", [] { Environment::from_name("preprod"); }); + expect_invalid("from_name case", [] { Environment::from_name("Production"); }); + } + + void test_local_override_is_valid() + { + auto env = Environment::production(); + env.name = "local"; + env.clob_url = "http://127.0.0.1:8080"; + env.clob_market_ws_url = "ws://127.0.0.1:8081/ws/market"; + env.validate(); + } + + void test_validate_rejects_bad_fields() + { + const auto mutated = [](auto mutate) + { + return [mutate] + { + auto env = Environment::production(); + mutate(env); + env.validate(); + }; + }; + expect_invalid("empty name", mutated([](Environment &e) { e.name.clear(); })); + expect_invalid("empty clob", mutated([](Environment &e) { e.clob_url.clear(); })); + expect_invalid("scheme only", mutated([](Environment &e) { e.gamma_url = "https://"; })); + expect_invalid("ws scheme for rest", + mutated([](Environment &e) { e.data_url = "wss://x"; })); + expect_invalid("http scheme for ws", + mutated([](Environment &e) { e.clob_user_ws_url = "https://x"; })); + expect_invalid("missing scheme", + mutated([](Environment &e) { e.rpc_url = "polygon.drpc.org"; })); + expect_invalid("bad contract", + mutated([](Environment &e) { e.contracts.standard_exchange = "0x12"; })); + expect_invalid("zero chain", mutated([](Environment &e) { e.contracts.chain_id = 0; })); + } + + // Config and PositionClientConfig repeat the production URLs as member + // defaults; keep them in step with the production preset. + void test_default_configs_match_production() + { + const auto production = Environment::production(); + const Config config; + const auto from_preset = Config::for_environment(production); + expect_equal("default clob rest", config.clob_rest_url, from_preset.clob_rest_url); + expect_equal("default market ws", config.clob_ws_url, from_preset.clob_ws_url); + expect_equal("default user ws", config.clob_user_ws_url, from_preset.clob_user_ws_url); + expect_equal("default gamma", config.gamma_api_url, from_preset.gamma_api_url); + expect_equal("default rtds", config.rtds_ws_url, from_preset.rtds_ws_url); + + const PositionClientConfig position; + expect_equal("default position gamma", position.gamma_api_url, production.gamma_url); + expect_equal("default position relayer", position.relayer_url, production.relayer_url); + check(position.contracts.chain_id == production.contracts.chain_id && + position.contracts.standard_exchange == production.contracts.standard_exchange && + position.contracts.collateral_token == production.contracts.collateral_token, + "default PositionClientConfig contracts must match production"); + check(position.rpc_url.empty(), "default PositionClientConfig must not pick an RPC"); + } + + void test_config_for_environment() + { + auto env = Environment::preproduction(); + env.clob_market_ws_url = "ws://127.0.0.1:9001/ws/market"; + env.clob_user_ws_url = "ws://127.0.0.1:9001/ws/user"; + env.rtds_ws_url = "ws://127.0.0.1:9002"; + const auto config = Config::for_environment(env); + const Config defaults; + expect_equal("config clob rest", config.clob_rest_url, env.clob_url); + expect_equal("config market ws", config.clob_ws_url, env.clob_market_ws_url); + expect_equal("config user ws", config.clob_user_ws_url, env.clob_user_ws_url); + expect_equal("config gamma", config.gamma_api_url, env.gamma_url); + expect_equal("config rtds", config.rtds_ws_url, env.rtds_ws_url); + check(config.http_timeout_ms == defaults.http_timeout_ms && + config.ws_ping_interval_ms == defaults.ws_ping_interval_ms && + config.max_markets == defaults.max_markets && + config.crypto_tickers == defaults.crypto_tickers, + "Config::for_environment must keep non-endpoint defaults"); + + env.gamma_url.clear(); + expect_invalid("config invalid environment", [&] { (void)Config::for_environment(env); }); + } + + void test_position_config_for_environment() + { + auto env = Environment::preproduction(); + env.rpc_url = "http://127.0.0.1:8545"; + env.contracts.standard_exchange = "0x1111111111111111111111111111111111111111"; + const auto config = PositionClientConfig::for_environment(env); + const PositionClientConfig defaults; + expect_equal("position rpc", config.rpc_url, env.rpc_url); + expect_equal("position gamma", config.gamma_api_url, env.gamma_url); + expect_equal("position relayer", config.relayer_url, env.relayer_url); + expect_equal("position exchange", config.contracts.standard_exchange, + env.contracts.standard_exchange); + check(config.contracts.chain_id == env.contracts.chain_id, + "PositionClientConfig::for_environment must copy the chain"); + check(config.private_key.empty() && config.funder_address.empty() && + config.wallet_type == defaults.wallet_type && config.relayer_api_key.empty() && + config.rpc_timeout_ms == defaults.rpc_timeout_ms && + config.relayer_retry_delay_ms == defaults.relayer_retry_delay_ms && + config.relayer_max_submit_retries == defaults.relayer_max_submit_retries, + "PositionClientConfig::for_environment must leave wallet and retry settings alone"); + + env.contracts.collateral_token = "0x12"; + expect_invalid("position invalid environment", + [&] { (void)PositionClientConfig::for_environment(env); }); + } +} // namespace + +int main() +{ + test_production_preset(); + test_preproduction_preset(); + test_from_name(); + test_local_override_is_valid(); + test_validate_rejects_bad_fields(); + test_default_configs_match_production(); + test_config_for_environment(); + test_position_config_for_environment(); + return check_support::finish("test_environment"); +} diff --git a/tests/test_preproduction_live.cpp b/tests/test_preproduction_live.cpp new file mode 100644 index 0000000..a29ccba --- /dev/null +++ b/tests/test_preproduction_live.cpp @@ -0,0 +1,36 @@ +#include "check_support.hpp" +#include "polymarket/clob_client.hpp" +#include "polymarket/environment.hpp" +#include +#include +#include + +// Preproduction runs on Polygon mainnet, so this test must stay read-only. +int main() +{ + using check_support::check; + using namespace polymarket; + + const char *run_live = std::getenv("POLYMARKET_RUN_LIVE_SMOKE"); + if (!run_live || std::string_view(run_live) != "1") + { + std::cout << "test_preproduction_live skipped: set " + "POLYMARKET_RUN_LIVE_SMOKE=1 to enable live preproduction calls\n"; + return 0; + } + + http_global_init(); + { + ClobClient client(Environment::preproduction()); + const auto server_time = client.get_server_time(); + check(server_time && *server_time > 0, "preproduction CLOB must report its server time"); + + const auto markets = client.get_markets(); + check(!markets.data.empty(), "preproduction CLOB must list markets"); + if (!markets.data.empty()) + check(!markets.data.front().condition_id.empty(), + "preproduction markets must carry condition IDs"); + } + http_global_cleanup(); + return check_support::finish("test_preproduction_live"); +}