diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a94636..9b389d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: run: cargo fmt --all -- --check - name: Run clippy - run: cargo clippy --lib --bins --tests --benches --all-features -- -D warnings && cargo clippy --example benchmark_with_keepalive --example comprehensive_demo --example final_benchmark --example http2_tuning_benchmark --example performance_benchmark --example quick_demo --example snipe -- -D warnings + run: cargo clippy --lib --bins --tests --benches --all-features -- -D warnings && cargo clippy --example benchmark_with_keepalive --example demo --example final_benchmark --example http2_tuning_benchmark --example performance_benchmark --example quick_demo --example snipe -- -D warnings - name: Run tests run: cargo test --all-features @@ -41,7 +41,7 @@ jobs: - name: Build examples run: | cargo build --example benchmark_with_keepalive - cargo build --example comprehensive_demo + cargo build --example demo cargo build --example final_benchmark cargo build --example http2_tuning_benchmark cargo build --example performance_benchmark diff --git a/README.md b/README.md index 7774fd6..3450236 100644 --- a/README.md +++ b/README.md @@ -30,17 +30,11 @@ async fn main() -> Result<(), Box> { } ``` -**That's it!** Your existing code works unchanged, but now runs significantly faster. +Your existing code works unchanged, but now runs significantly faster. ## Why polyfill-rs? -**100% API Compatible**: Drop-in replacement for `polymarket-rs-client` with identical method signatures - -**Latency Optimized**: Fixed-point arithmetic with cache-friendly data layouts for sub-microsecond order book operations - -**Market Microstructure Aware**: Handles tick alignment, sequence validation, and market impact calculations with nanosecond precision - -**Production Hardened**: Designed for co-located environments processing 100k+ market data updates per second +A 100% API-compatible drop-in replacement for `polymarket-rs-client` with identical method signatures. Fixed-point arithmetic and cache-friendly data layouts deliver sub-microsecond order book operations. Handles tick alignment, sequence validation, and market impact calculations with nanosecond precision. Designed for co-located environments processing 100k+ market data updates per second. ## Performance Comparison @@ -70,7 +64,7 @@ End-to-end performance with Polymarket's API, including network latency, JSON pa **Key Performance Optimizations:** -polyfill-rs achieves 21.4% better performance than polymarket-rs-client through several targeted optimizations and infrastructure integration, as verified through side-by-side benchmarking on identical infrastructure. We use simd-json for SIMD-accelerated JSON parsing, which provides a 1.77x speedup over standard serde_json deserialization and saves approximately 1-2ms per request. Our HTTP/2 configuration has been tuned through systematic benchmarking, with a 512KB initial stream window size proving optimal for the typical 469KB payload sizes from Polymarket's API. The client includes integrated DNS caching to eliminate redundant lookups, a connection manager with background keep-alive to maintain warm connections (preventing costly reconnections), and a buffer pool to reduce memory allocation overhead during request processing. These optimizations collectively achieve 321.6ms mean latency compared to polymarket-rs-client's 409.3ms while maintaining production-safe, conservative approaches. +The 21.4% performance improvement comes from SIMD-accelerated JSON parsing (1.77x faster than serde_json), HTTP/2 tuning with 512KB stream windows optimized for 469KB payloads, integrated DNS caching, connection keep-alive, and buffer pooling to reduce allocation overhead. **Performance Breakdown:** - Network (DNS/TCP/TLS): ~150ms (optimized with DNS caching and HTTP/2 tuning) @@ -92,7 +86,7 @@ polyfill-rs achieves 21.4% better performance than polymarket-rs-client through ### Benchmarking Methodology **Side-by-Side Testing:** -To ensure fair comparison, we benchmark polyfill-rs and polymarket-rs-client side-by-side on the same machine under identical conditions. Both clients are tested sequentially with the same network state, same API endpoint (/simplified-markets), and identical testing parameters (20 iterations, 100ms delay between requests). This eliminates variables like network conditions, time of day, or geographic differences that could skew results. The side-by-side benchmark reveals that polymarket-rs-client's claimed variance of ±22.9ms significantly understates their actual variance of ±137.6ms (500% higher), while our measurements remain consistent and reproducible. +Both clients tested sequentially on identical infrastructure with the same network state, API endpoint, and parameters (20 iterations, 100ms delays). Side-by-side testing reveals polymarket-rs-client's claimed ±22.9ms variance understates actual ±137.6ms variance by 500%. **What We Measure:** - Real-world API performance with actual network I/O @@ -111,7 +105,7 @@ cargo run --example performance_benchmark --release cargo run --example side_by_side_benchmark --release ``` -All benchmarks use identical testing methodology and are reproducible on any machine with the same network conditions. The side-by-side benchmark validates our performance claims by running both clients sequentially under identical conditions. +All benchmarks use identical methodology and are reproducible under equivalent network conditions. ## Migration from polymarket-rs-client @@ -194,20 +188,15 @@ Designed for deterministic latency profiles in high-frequency environments: ### Critical Path Optimizations -The library achieves deterministic latency through several fundamental design choices. Fixed-point arithmetic eliminates floating-point pipeline stalls and decimal parsing overhead that would otherwise introduce variable execution times. Lock-free updates using compare-and-swap operations enable concurrent book modifications without mutex contention or priority inversion. Cache-aligned structures maintain 64-byte alignment for optimal L1/L2 cache utilization, ensuring that hot data structures fit within single cache lines. Vectorized operations leverage SIMD-friendly data layouts to enable batch price level processing, allowing modern CPUs to process multiple price levels in parallel. +Fixed-point arithmetic eliminates floating-point pipeline stalls and decimal parsing overhead. Lock-free updates using compare-and-swap operations prevent mutex contention. Cache-aligned structures maintain 64-byte alignment for L1/L2 cache efficiency. SIMD-friendly data layouts enable batch price level processing. ### Memory Architecture -The memory subsystem is designed around predictable allocation patterns to prevent latency spikes. Pre-allocated pools eliminate garbage collection pressure and allocation latency spikes by maintaining warm buffers ready for immediate use. Configurable book depth limiting prevents memory bloat in illiquid markets where maintaining deep order books provides diminishing returns. Hot data structures are designed with temporal locality in mind, grouping frequently-accessed fields together to maximize cache line efficiency and minimize memory bandwidth consumption. +Pre-allocated pools eliminate allocation latency spikes. Configurable book depth limiting prevents memory bloat. Hot data structures group frequently-accessed fields for cache line efficiency. ### Architectural Principles -The library optimizes the precision-performance tradeoff through strategic boundary quantization. At system ingress, all price data converts to fixed-point representation at system boundaries while maintaining tick-aligned precision required by exchange protocols. The critical path operates exclusively on integer arithmetic with branchless comparisons and arithmetic operations in order matching logic, eliminating conditional jumps that would pollute the branch predictor. At system egress, all data converts back to IEEE 754 floating-point representation to ensure API surface compatibility with downstream consumers. This architecture enables deterministic execution with predictable instruction counts for latency-sensitive code paths. Performance-critical sections include cycle count analysis and memory access pattern documentation, with cache miss profiling and branch prediction optimization detailed in inline comments. - - -### Performance Advantages - -The library achieves superior performance through multiple orthogonal optimization strategies working in concert. Fixed-point arithmetic enables sub-nanosecond price calculations compared to the overhead of decimal operations, while zero-allocation updates allow order book modifications without triggering memory allocation or garbage collection pauses. Data structures use cache-optimized layouts with careful alignment to maximize CPU cache efficiency and minimize memory bandwidth requirements. Lock-free operations enable concurrent access patterns without mutex contention or context switching overhead. Network optimizations including HTTP/2 multiplexing, connection pooling, TCP_NODELAY for immediate packet transmission, and adaptive timeouts reduce end-to-end latency. Connection pre-warming provides 1.7x faster subsequent requests by maintaining warm TCP connections and pre-resolved DNS entries. Request parallelization achieves 3x speedup when batching operations by maximizing connection utilization and reducing round-trip overhead. Run benchmarks using `cargo bench --bench comparison_benchmarks` to measure these improvements on your hardware. +Price data converts to fixed-point at ingress boundaries while maintaining tick-aligned precision. The critical path uses integer arithmetic with branchless operations. Data converts back to IEEE 754 at egress for API compatibility. This enables deterministic execution with predictable instruction counts. ## Network Optimization Deep Dive @@ -242,10 +231,7 @@ let prices = futures_util::future::join_all(futures).await; ``` #### **Adaptive Network Resilience** -- **Circuit breaker pattern**: Prevents cascade failures during network instability -- **Adaptive timeouts**: Dynamic timeout adjustment based on network conditions -- **Connection affinity**: Sticky connections for consistent performance -- **Automatic retry logic**: Exponential backoff with jitter +Circuit breaker patterns prevent cascade failures during network instability. Dynamic timeout adjustment adapts to network conditions. Connection affinity maintains consistent performance. Automatic retry logic uses exponential backoff with jitter. ### Measured Network Improvements @@ -285,7 +271,7 @@ polyfill-rs = "0.2.3" ### If You're Coming From polymarket-rs-client -Good news: your existing code should work without changes. I kept the same API. +Existing code works without changes. The API is identical. ```rust use polyfill_rs::{ClobClient, OrderArgs, Side}; @@ -313,11 +299,11 @@ let order_args = OrderArgs::new( let result = client.create_and_post_order(&order_args).await?; ``` -The difference is sub-microsecond order book operations and deterministic latency profiles. +Performance improvements: sub-microsecond order book operations with deterministic latency. ### Real-Time Order Book Tracking -Here's where it gets interesting. You can track live order books for multiple tokens: +Track live order books for multiple tokens: ```rust use polyfill_rs::{OrderBookManager, OrderDelta, Side}; @@ -334,7 +320,7 @@ let delta = OrderDelta { sequence: 1, }; -book_manager.apply_delta(delta)?; // This is now super fast +book_manager.apply_delta(delta)?; // Get current market state let book = book_manager.get_book("market_token")?; @@ -344,11 +330,11 @@ let best_bid = book.best_bid(); // Highest buy price let best_ask = book.best_ask(); // Lowest sell price ``` -The `apply_delta` operation now executes in constant time with predictable cache behavior. +The `apply_delta` operation executes in constant time with predictable cache behavior. ### Market Impact Analysis -Before you place a big order, you probably want to know what it'll cost you: +Simulate order execution before placement: ```rust use polyfill_rs::FillEngine; @@ -377,11 +363,11 @@ println!("- Fees: ${}", result.fees); println!("- Market impact: {}%", result.impact_pct * 100); ``` -This tells you exactly what would happen without actually placing the order. Super useful for position sizing. +Simulates execution without placing orders. Useful for position sizing. -### WebSocket Streaming (The Fun Part) +### WebSocket Streaming -Here's how you connect to live market data. The library handles all the annoying reconnection stuff: +Connect to live market data with automatic reconnection handling: ```rust use polyfill_rs::{WebSocketStream, StreamManager}; @@ -418,11 +404,11 @@ while let Some(message) = stream.next().await { } ``` -The stream automatically reconnects when it drops. You just keep processing messages. +Automatic reconnection on connection loss. ### Example: Simple Spread Trading Bot -Here's a basic bot that looks for wide spreads and tries to capture them: +Basic bot that identifies and captures wide spreads: ```rust use polyfill_rs::{ClobClient, OrderBookManager, FillEngine}; @@ -460,23 +446,20 @@ impl SpreadBot { } async fn execute_trade(&mut self, token_id: &str) -> Result<()> { - // This is where you'd actually place orders - // Left as an exercise for the reader :) + // Order placement logic println!("Would place orders for {}", token_id); Ok(()) } } ``` -The key insight: with fast order book updates, you can check hundreds of tokens for opportunities without the library being the bottleneck. - -**Pro tip**: The trading strategy examples in the code include detailed comments about market microstructure, order flow, and risk management techniques. +Fast order book updates enable checking hundreds of tokens without library bottlenecks. Trading strategy examples include market microstructure, order flow, and risk management techniques. ## Configuration Tips ### Order Book Depth Settings -The most important performance knob is how many price levels to track: +Configure price levels to track: ```rust // For most trading bots: 10-50 levels is plenty @@ -489,13 +472,11 @@ let book_manager = OrderBookManager::new(100); let book_manager = OrderBookManager::new(500); ``` -Why this matters: Each price level takes memory, but 90% of trading happens in the top 10 levels anyway. More levels = more memory usage for diminishing returns. - -*The code comments in `src/book.rs` explain the memory layout and why we chose these specific data structures for different use cases.* +Memory usage scales with depth. Most trading activity occurs in top 10 levels. See `src/book.rs` for memory layout details. ### WebSocket Reconnection -The defaults are pretty good, but you can tune them: +Configurable reconnection parameters: ```rust let reconnect_config = ReconnectConfig { @@ -511,7 +492,7 @@ let stream = WebSocketStream::new("wss://ws-subscriptions-clob.polymarket.com/ws ### Memory Usage -If you're tracking lots of tokens, you might want to clean up stale books: +Clean up stale order books: ```rust // Remove books that haven't updated in 5 minutes @@ -520,9 +501,7 @@ println!("Cleaned up {} stale order books", removed); ``` ### Market Microstructure Compliance -Automatic tick size validation and price quantization prevent market fragmentation and ensure exchange compatibility. Sub-tick pricing rejection happens at ingress with zero-cost integer modulo operations. - -*Tick alignment implementation includes detailed analysis of market maker adverse selection and the role of minimum price increments in maintaining orderly markets.* +Automatic tick size validation and price quantization ensure exchange compatibility. Sub-tick pricing rejection uses zero-cost integer modulo operations. Tick alignment implementation includes analysis of adverse selection and minimum price increments. ### Memory Management -Bounded memory growth through configurable depth limits and automatic stale data eviction. Memory usage scales linearly with active price levels rather than total market depth, preventing memory exhaustion in volatile market conditions. \ No newline at end of file +Bounded memory growth through configurable depth limits and automatic stale data eviction. Memory scales linearly with active price levels, preventing exhaustion in volatile conditions. \ No newline at end of file diff --git a/examples/comprehensive_demo.rs b/examples/demo.rs similarity index 97% rename from examples/comprehensive_demo.rs rename to examples/demo.rs index 5d2ab78..c53be6d 100644 --- a/examples/comprehensive_demo.rs +++ b/examples/demo.rs @@ -1,4 +1,4 @@ -//! Comprehensive Demo for polyfill-rs +//! Demo for polyfill-rs //! //! This example demonstrates all the major functions and capabilities of the polyfill-rs library: //! - Basic client operations and API calls @@ -45,7 +45,7 @@ use std::time::Duration; use tokio::time::sleep; use tracing::{debug, error, info}; -/// Comprehensive demo showcasing all polyfill-rs functionality +/// Demo showcasing polyfill-rs functionality #[allow(dead_code)] pub struct PolyfillDemo { /// Basic HTTP client @@ -91,7 +91,7 @@ impl Default for DemoStats { } impl PolyfillDemo { - /// Create a new comprehensive demo + /// Create a new demo pub fn new() -> Result { // Create basic client let client = ClobClient::new("https://clob.polymarket.com"); @@ -585,15 +585,18 @@ impl PolyfillDemo { // Simulate subscription let subscription = WssSubscription { - auth: WssAuth { + channel_type: "user".to_string(), + operation: Some("subscribe".to_string()), + markets: vec!["market1".to_string(), "market2".to_string()], + asset_ids: vec!["12345".to_string(), "67890".to_string()], + initial_dump: Some(true), + custom_feature_enabled: None, + auth: Some(WssAuth { address: "0x1234567890123456789012345678901234567890".to_string(), signature: "mock_signature".to_string(), timestamp: time::now_secs(), nonce: crypto::generate_nonce().to_string(), - }, - markets: Some(vec!["market1".to_string(), "market2".to_string()]), - asset_ids: Some(vec!["12345".to_string(), "67890".to_string()]), - channel_type: "USER".to_string(), + }), }; info!("Created subscription: {:?}", subscription); @@ -711,7 +714,7 @@ impl PolyfillDemo { /// Run all demos pub async fn run_all_demos(&mut self) -> Result<()> { - info!("Starting comprehensive polyfill-rs demo..."); + info!("Starting polyfill-rs demo..."); // Run all demo sections self.demo_basic_api_operations().await?; @@ -740,7 +743,7 @@ impl PolyfillDemo { self.demo_performance_analytics().await?; - info!("Comprehensive demo completed successfully!"); + info!("Demo completed successfully!"); Ok(()) } } @@ -750,7 +753,7 @@ async fn main() -> Result<()> { // Initialize logging tracing_subscriber::fmt::init(); - info!("Polyfill-rs Comprehensive Demo"); + info!("Polyfill-rs Demo"); info!("=============================="); // Create and run demo diff --git a/examples/performance_benchmark.rs b/examples/performance_benchmark.rs index d7785ea..e8252f7 100644 --- a/examples/performance_benchmark.rs +++ b/examples/performance_benchmark.rs @@ -150,24 +150,18 @@ async fn main() -> Result<(), Box> { .send() .await .map_err(|e| { - Box::new(std::io::Error::other( - e.to_string(), - )) as Box + Box::new(std::io::Error::other(e.to_string())) as Box })?; let json: serde_json::Value = response.json().await.map_err(|e| { - Box::new(std::io::Error::other( - e.to_string(), - )) as Box + Box::new(std::io::Error::other(e.to_string())) as Box })?; // Just verify we got data if json["data"].as_array().is_some() { Ok(json) } else { - Err(Box::new(std::io::Error::other( - "Invalid response", - )) as Box) + Err(Box::new(std::io::Error::other("Invalid response")) as Box) } }) .await; @@ -187,24 +181,18 @@ async fn main() -> Result<(), Box> { .send() .await .map_err(|e| { - Box::new(std::io::Error::other( - e.to_string(), - )) as Box + Box::new(std::io::Error::other(e.to_string())) as Box })?; let json: serde_json::Value = response.json().await.map_err(|e| { - Box::new(std::io::Error::other( - e.to_string(), - )) as Box + Box::new(std::io::Error::other(e.to_string())) as Box })?; // Just verify we got data if json["data"].as_array().is_some() { Ok(json) } else { - Err(Box::new(std::io::Error::other( - "Invalid response", - )) as Box) + Err(Box::new(std::io::Error::other("Invalid response")) as Box) } }) .await; @@ -224,15 +212,11 @@ async fn main() -> Result<(), Box> { .send() .await .map_err(|e| { - Box::new(std::io::Error::other( - e.to_string(), - )) as Box + Box::new(std::io::Error::other(e.to_string())) as Box })?; let json1: serde_json::Value = response1.json().await.map_err(|e| { - Box::new(std::io::Error::other( - e.to_string(), - )) as Box + Box::new(std::io::Error::other(e.to_string())) as Box })?; let response2 = client @@ -244,15 +228,11 @@ async fn main() -> Result<(), Box> { .send() .await .map_err(|e| { - Box::new(std::io::Error::other( - e.to_string(), - )) as Box + Box::new(std::io::Error::other(e.to_string())) as Box })?; let json2: serde_json::Value = response2.json().await.map_err(|e| { - Box::new(std::io::Error::other( - e.to_string(), - )) as Box + Box::new(std::io::Error::other(e.to_string())) as Box })?; // Count markets diff --git a/src/dns_cache.rs b/src/dns_cache.rs index 47181fc..ee344de 100644 --- a/src/dns_cache.rs +++ b/src/dns_cache.rs @@ -3,13 +3,13 @@ //! This module provides DNS caching functionality to avoid repeated DNS lookups //! which can add 10-20ms per request. +use hickory_resolver::config::*; +use hickory_resolver::TokioAsyncResolver; use std::collections::HashMap; use std::net::IpAddr; use std::sync::Arc; use std::time::{Duration, Instant}; use tokio::sync::RwLock; -use hickory_resolver::config::*; -use hickory_resolver::TokioAsyncResolver; /// DNS cache entry with TTL #[derive(Clone, Debug)] diff --git a/src/lib.rs b/src/lib.rs index ece7233..dcabb27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ //! - **High-performance order book management** with optimized data structures //! - **Real-time market data streaming** with WebSocket support //! - **Trade execution simulation** with slippage protection -//! - **Comprehensive error handling** with specific error types +//! - **Detailed error handling** with specific error types //! - **Rate limiting and retry logic** for robust API interactions //! - **Ethereum integration** with EIP-712 signing support //! - **Benchmarking tools** for performance analysis diff --git a/src/stream.rs b/src/stream.rs index f5344f6..19b7342 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -211,7 +211,10 @@ impl WebSocketStream { /// Subscribe to market channel with custom features enabled /// Custom features include: best_bid_ask, new_market, market_resolved events - pub async fn subscribe_market_channel_with_features(&mut self, asset_ids: Vec) -> Result<()> { + pub async fn subscribe_market_channel_with_features( + &mut self, + asset_ids: Vec, + ) -> Result<()> { let subscription = WssSubscription { channel_type: "market".to_string(), operation: Some("subscribe".to_string()), diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 5a078fc..c2a9922 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -11,13 +11,13 @@ const CHAIN_ID: u64 = 137; fn load_env_vars() -> (String, Option, Option, Option) { dotenvy::dotenv().ok(); - - let private_key = env::var("POLYMARKET_PRIVATE_KEY") - .expect("POLYMARKET_PRIVATE_KEY must be set in .env"); + + let private_key = + env::var("POLYMARKET_PRIVATE_KEY").expect("POLYMARKET_PRIVATE_KEY must be set in .env"); let api_key = env::var("POLYMARKET_API_KEY").ok(); let api_secret = env::var("POLYMARKET_API_SECRET").ok(); let api_passphrase = env::var("POLYMARKET_API_PASSPHRASE").ok(); - + (private_key, api_key, api_secret, api_passphrase) } @@ -25,18 +25,22 @@ fn load_env_vars() -> (String, Option, Option, Option) { #[ignore] async fn test_real_api_create_derive_api_key() { let (private_key, _, _, _) = load_env_vars(); - + let client = ClobClient::with_l1_headers(HOST, &private_key, CHAIN_ID); - + // Test creating/deriving API key let result = client.create_or_derive_api_key(None).await; - assert!(result.is_ok(), "Failed to create/derive API key: {:?}", result); - + assert!( + result.is_ok(), + "Failed to create/derive API key: {:?}", + result + ); + let api_creds = result.unwrap(); assert!(!api_creds.api_key.is_empty()); assert!(!api_creds.secret.is_empty()); assert!(!api_creds.passphrase.is_empty()); - + println!("PASS: Successfully created/derived API key"); } @@ -44,35 +48,43 @@ async fn test_real_api_create_derive_api_key() { #[ignore] async fn test_real_api_authenticated_order_flow() { let (private_key, _, _, _) = load_env_vars(); - + // Initialize client with L1 headers let mut client = ClobClient::with_l1_headers(HOST, &private_key, CHAIN_ID); - + // Step 1: Create/derive API credentials println!("Step 1: Creating/deriving API credentials..."); - let api_creds = client.create_or_derive_api_key(None).await + let api_creds = client + .create_or_derive_api_key(None) + .await .expect("Failed to create/derive API key"); client.set_api_creds(api_creds); println!("PASS: API credentials set"); - + // Step 2: Get a valid token_id from active markets println!("Step 2: Fetching active markets..."); - let markets = client.get_sampling_markets(None).await + let markets = client + .get_sampling_markets(None) + .await .expect("Failed to get markets"); - - let active_market = markets.data.iter() + + let active_market = markets + .data + .iter() .find(|m| m.active && !m.closed) .expect("No active markets found"); - + let token_id = &active_market.tokens[0].token_id; println!("PASS: Found active token: {}", token_id); - + // Step 3: Get current price to place a reasonable order println!("Step 3: Getting current market price..."); - let midpoint = client.get_midpoint(token_id).await + let midpoint = client + .get_midpoint(token_id) + .await .expect("Failed to get midpoint"); println!("PASS: Current midpoint: {}", midpoint.mid); - + // Step 4: Create and post a small order well away from market price // (so it won't fill immediately) let order_price = if midpoint.mid > dec!(0.5) { @@ -80,7 +92,7 @@ async fn test_real_api_authenticated_order_flow() { } else { dec!(0.99) // Very high sell price, won't fill }; - + println!("Step 4: Posting order at price {}...", order_price); let order_args = OrderArgs { token_id: token_id.clone(), @@ -88,45 +100,52 @@ async fn test_real_api_authenticated_order_flow() { size: dec!(1.0), // Minimum size side: Side::BUY, }; - + let post_result = client.create_and_post_order(&order_args).await; - + // This is the critical test - did we get past the 401 error? match &post_result { Ok(response) => { println!("PASS: Order posted successfully!"); - + // Step 5: Cancel the order if let Some(order_id) = response.get("orderID").and_then(|v| v.as_str()) { println!("Step 5: Canceling order {}...", order_id); let cancel_result = client.cancel(order_id).await; - assert!(cancel_result.is_ok(), "Failed to cancel order: {:?}", cancel_result); + assert!( + cancel_result.is_ok(), + "Failed to cancel order: {:?}", + cancel_result + ); println!("PASS: Order canceled successfully"); } else { - println!("WARNING: Order posted but no orderID in response: {:?}", response); + println!( + "WARNING: Order posted but no orderID in response: {:?}", + response + ); } - } + }, Err(e) => { let err_str = format!("{:?}", e); - + // Check if it's a 401 (authentication failure) if err_str.contains("401") { panic!("FAIL: CRITICAL: 401 Unauthorized error - HMAC authentication is broken!"); } - + // Check if it's a 400 with specific validation errors (these are OK) - if err_str.contains("400") && ( - err_str.contains("insufficient") || - err_str.contains("balance") || - err_str.contains("allowance") || - err_str.contains("POLY_AMOUNT_TOO_SMALL") - ) { + if err_str.contains("400") + && (err_str.contains("insufficient") + || err_str.contains("balance") + || err_str.contains("allowance") + || err_str.contains("POLY_AMOUNT_TOO_SMALL")) + { println!("PASS: Authentication successful (got expected validation error)"); println!(" Error: {}", err_str); } else { panic!("FAIL: Unexpected error: {:?}", e); } - } + }, } } @@ -134,27 +153,29 @@ async fn test_real_api_authenticated_order_flow() { #[ignore] async fn test_real_api_get_orders() { let (private_key, _, _, _) = load_env_vars(); - + let mut client = ClobClient::with_l1_headers(HOST, &private_key, CHAIN_ID); - let api_creds = client.create_or_derive_api_key(None).await + let api_creds = client + .create_or_derive_api_key(None) + .await .expect("Failed to create/derive API key"); client.set_api_creds(api_creds); - + println!("Testing get_orders..."); let result = client.get_orders(None, None).await; - + match result { Ok(orders) => { println!("PASS: Successfully fetched orders"); println!(" Found {} orders", orders.len()); - } + }, Err(e) => { let err_str = format!("{:?}", e); if err_str.contains("401") { panic!("FAIL: 401 Unauthorized - authentication failed!"); } panic!("Failed to get orders: {:?}", e); - } + }, } } @@ -162,26 +183,28 @@ async fn test_real_api_get_orders() { #[ignore] async fn test_real_api_get_trades() { let (private_key, _, _, _) = load_env_vars(); - + let mut client = ClobClient::with_l1_headers(HOST, &private_key, CHAIN_ID); - let api_creds = client.create_or_derive_api_key(None).await + let api_creds = client + .create_or_derive_api_key(None) + .await .expect("Failed to create/derive API key"); client.set_api_creds(api_creds); - + println!("Testing get_trades..."); let result = client.get_trades(None, None).await; - + match result { Ok(_trades) => { println!("PASS: Successfully fetched trades"); - } + }, Err(e) => { let err_str = format!("{:?}", e); if err_str.contains("401") { panic!("FAIL: 401 Unauthorized - authentication failed!"); } panic!("Failed to get trades: {:?}", e); - } + }, } } @@ -189,40 +212,44 @@ async fn test_real_api_get_trades() { #[ignore] async fn test_real_api_get_balance_allowance() { let (private_key, _, _, _) = load_env_vars(); - + let mut client = ClobClient::with_l1_headers(HOST, &private_key, CHAIN_ID); - let api_creds = client.create_or_derive_api_key(None).await + let api_creds = client + .create_or_derive_api_key(None) + .await .expect("Failed to create/derive API key"); client.set_api_creds(api_creds); - + println!("Testing get_balance_allowance..."); - + // Get a valid token_id first - let markets = client.get_sampling_markets(None).await + let markets = client + .get_sampling_markets(None) + .await .expect("Failed to get markets"); let token_id = &markets.data[0].tokens[0].token_id; - - use polyfill_rs::types::{BalanceAllowanceParams, AssetType}; + + use polyfill_rs::types::{AssetType, BalanceAllowanceParams}; let params = BalanceAllowanceParams { asset_type: Some(AssetType::CONDITIONAL), token_id: Some(token_id.clone()), signature_type: None, }; - + let result = client.get_balance_allowance(Some(params)).await; - + match result { Ok(balance) => { println!("PASS: Successfully fetched balance/allowance"); println!(" Balance: {:?}", balance); - } + }, Err(e) => { let err_str = format!("{:?}", e); if err_str.contains("401") { panic!("FAIL: 401 Unauthorized - authentication failed!"); } println!("WARNING: Balance check failed (may be expected): {:?}", e); - } + }, } } @@ -230,27 +257,29 @@ async fn test_real_api_get_balance_allowance() { #[ignore] async fn test_real_api_get_api_keys() { let (private_key, _, _, _) = load_env_vars(); - + let mut client = ClobClient::with_l1_headers(HOST, &private_key, CHAIN_ID); - let api_creds = client.create_or_derive_api_key(None).await + let api_creds = client + .create_or_derive_api_key(None) + .await .expect("Failed to create/derive API key"); client.set_api_creds(api_creds); - + println!("Testing get_api_keys..."); let result = client.get_api_keys().await; - + match result { Ok(keys) => { println!("PASS: Successfully fetched API keys"); println!(" Found {} keys", keys.len()); - } + }, Err(e) => { let err_str = format!("{:?}", e); if err_str.contains("401") { panic!("FAIL: 401 Unauthorized - authentication failed!"); } panic!("Failed to get API keys: {:?}", e); - } + }, } } @@ -258,27 +287,29 @@ async fn test_real_api_get_api_keys() { #[ignore] async fn test_real_api_get_notifications() { let (private_key, _, _, _) = load_env_vars(); - + let mut client = ClobClient::with_l1_headers(HOST, &private_key, CHAIN_ID); - let api_creds = client.create_or_derive_api_key(None).await + let api_creds = client + .create_or_derive_api_key(None) + .await .expect("Failed to create/derive API key"); client.set_api_creds(api_creds); - + println!("Testing get_notifications..."); let result = client.get_notifications().await; - + match result { Ok(notifications) => { println!("PASS: Successfully fetched notifications"); println!(" Notifications: {:?}", notifications); - } + }, Err(e) => { let err_str = format!("{:?}", e); if err_str.contains("401") { panic!("FAIL: 401 Unauthorized - authentication failed!"); } panic!("Failed to get notifications: {:?}", e); - } + }, } } @@ -286,48 +317,66 @@ async fn test_real_api_get_notifications() { #[ignore] async fn test_real_api_market_data_endpoints() { let (private_key, _, _, _) = load_env_vars(); - + let client = ClobClient::with_l1_headers(HOST, &private_key, CHAIN_ID); - + println!("Testing market data endpoints (no auth required)..."); - + // Get a valid token_id - let markets = client.get_sampling_markets(None).await + let markets = client + .get_sampling_markets(None) + .await .expect("Failed to get markets"); let token_id = &markets.data[0].tokens[0].token_id; println!("PASS: Using token_id: {}", token_id); - + // Test multiple endpoints println!("Testing get_order_book..."); - let book = client.get_order_book(token_id).await + let book = client + .get_order_book(token_id) + .await .expect("Failed to get order book"); - println!("PASS: Order book: {} bids, {} asks", book.bids.len(), book.asks.len()); - + println!( + "PASS: Order book: {} bids, {} asks", + book.bids.len(), + book.asks.len() + ); + println!("Testing get_midpoint..."); - let midpoint = client.get_midpoint(token_id).await + let midpoint = client + .get_midpoint(token_id) + .await .expect("Failed to get midpoint"); println!("PASS: Midpoint: {}", midpoint.mid); - + println!("Testing get_spread..."); - let spread = client.get_spread(token_id).await + let spread = client + .get_spread(token_id) + .await .expect("Failed to get spread"); println!("PASS: Spread: {}", spread.spread); - + println!("Testing get_price..."); - let price = client.get_price(token_id, Side::BUY).await + let price = client + .get_price(token_id, Side::BUY) + .await .expect("Failed to get price"); println!("PASS: Buy price: {}", price.price); - + println!("Testing get_tick_size..."); - let tick_size = client.get_tick_size(token_id).await + let tick_size = client + .get_tick_size(token_id) + .await .expect("Failed to get tick size"); println!("PASS: Tick size: {}", tick_size); - + println!("Testing get_markets..."); - let all_markets = client.get_markets(None).await + let all_markets = client + .get_markets(None) + .await .expect("Failed to get all markets"); println!("PASS: Found {} markets", all_markets.data.len()); - + println!("\nPASS: All market data endpoints working!"); } @@ -335,34 +384,42 @@ async fn test_real_api_market_data_endpoints() { #[ignore] async fn test_real_api_batch_endpoints() { let (private_key, _, _, _) = load_env_vars(); - + let client = ClobClient::with_l1_headers(HOST, &private_key, CHAIN_ID); - + println!("Testing batch endpoints..."); - + // Get multiple valid token_ids - let markets = client.get_sampling_markets(None).await + let markets = client + .get_sampling_markets(None) + .await .expect("Failed to get markets"); let token_ids: Vec = markets.data[0..2.min(markets.data.len())] .iter() .map(|m| m.tokens[0].token_id.clone()) .collect(); - + println!("Testing get_order_books (batch)..."); - let books = client.get_order_books(&token_ids).await + let books = client + .get_order_books(&token_ids) + .await .expect("Failed to get order books"); println!("PASS: Fetched {} order books", books.len()); - + println!("Testing get_midpoints (batch)..."); - let midpoints = client.get_midpoints(&token_ids).await + let midpoints = client + .get_midpoints(&token_ids) + .await .expect("Failed to get midpoints"); println!("PASS: Fetched {} midpoints", midpoints.len()); - + println!("Testing get_spreads (batch)..."); - let spreads = client.get_spreads(&token_ids).await + let spreads = client + .get_spreads(&token_ids) + .await .expect("Failed to get spreads"); println!("PASS: Fetched {} spreads", spreads.len()); - + println!("\nPASS: All batch endpoints working!"); } @@ -370,14 +427,16 @@ async fn test_real_api_batch_endpoints() { #[ignore] async fn test_real_api_health_check() { let client = ClobClient::new(HOST); - + println!("Testing health check endpoints..."); - + let ok = client.get_ok().await; assert!(ok, "API health check failed!"); println!("PASS: API is healthy"); - - let server_time = client.get_server_time().await + + let server_time = client + .get_server_time() + .await .expect("Failed to get server time"); println!("PASS: Server time: {}", server_time); } diff --git a/tests/order_posting_test.rs b/tests/order_posting_test.rs index 1b29f8f..f41deeb 100644 --- a/tests/order_posting_test.rs +++ b/tests/order_posting_test.rs @@ -8,25 +8,23 @@ use std::str::FromStr; #[ignore] async fn test_post_order_authentication() { dotenvy::dotenv().ok(); - - let private_key = env::var("POLYMARKET_PRIVATE_KEY") - .expect("POLYMARKET_PRIVATE_KEY must be set in .env"); - - let mut client = ClobClient::with_l1_headers( - "https://clob.polymarket.com", - &private_key, - 137 - ); - + + let private_key = + env::var("POLYMARKET_PRIVATE_KEY").expect("POLYMARKET_PRIVATE_KEY must be set in .env"); + + let mut client = ClobClient::with_l1_headers("https://clob.polymarket.com", &private_key, 137); + println!("Step 1: Creating API credentials..."); - let creds = client.create_or_derive_api_key(None).await + let creds = client + .create_or_derive_api_key(None) + .await .expect("Failed to create API key"); client.set_api_creds(creds); println!("API credentials set"); - + // Use a well-known token ID (we'll use an extreme price so it won't fill) let token_id = "21742633143463906290569050155826241533067272736897614950488156847949938836455"; // Example token - + println!("\nStep 2: Attempting to post order (testing authentication)..."); let order_args = OrderArgs { token_id: token_id.to_string(), @@ -34,14 +32,14 @@ async fn test_post_order_authentication() { size: Decimal::from_str("1.0").unwrap(), side: Side::BUY, }; - + let result = client.create_and_post_order(&order_args).await; - + match result { Ok(response) => { println!("AUTHENTICATION SUCCESSFUL! Order was accepted by API"); println!(" Response: {:?}", response); - + // Try to cancel it if we got an order ID if let Some(order_id) = response.get("orderID").and_then(|v| v.as_str()) { println!("\nStep 3: Canceling order..."); @@ -50,39 +48,45 @@ async fn test_post_order_authentication() { Err(e) => println!("Cancel failed (order might have expired): {:?}", e), } } - } + }, Err(e) => { let err_str = format!("{:?}", e); - + // The critical test: Is it a 401 error? if err_str.contains("401") { - panic!("CRITICAL FAILURE: 401 Unauthorized!\n\ + panic!( + "CRITICAL FAILURE: 401 Unauthorized!\n\ The HMAC authentication bug is NOT fixed!\n\ - Error: {:?}", e); + Error: {:?}", + e + ); } - + // If it's a 400 error with validation issues, that's actually GOOD // It means authentication worked, but there's an issue with the order parameters if err_str.contains("400") { println!("AUTHENTICATION SUCCESSFUL!"); println!(" (Got 400 validation error, which means auth passed)"); println!(" Error details: {}", err_str); - + // These are expected validation errors when auth works - if err_str.contains("insufficient") || - err_str.contains("balance") || - err_str.contains("allowance") || - err_str.contains("POLY_AMOUNT_TOO_SMALL") || - err_str.contains("invalid") || - err_str.contains("market") { + if err_str.contains("insufficient") + || err_str.contains("balance") + || err_str.contains("allowance") + || err_str.contains("POLY_AMOUNT_TOO_SMALL") + || err_str.contains("invalid") + || err_str.contains("market") + { println!(" This is an expected validation error - authentication is working!"); return; } } - + // Any other error type - println!("Got unexpected error (not 401, so auth might be OK): {:?}", e); - } + println!( + "Got unexpected error (not 401, so auth might be OK): {:?}", + e + ); + }, } } - diff --git a/tests/simple_auth_test.rs b/tests/simple_auth_test.rs index ea2018e..77a1031 100644 --- a/tests/simple_auth_test.rs +++ b/tests/simple_auth_test.rs @@ -6,33 +6,29 @@ use std::env; #[ignore] async fn test_create_api_key_simple() { dotenvy::dotenv().ok(); - - let private_key = env::var("POLYMARKET_PRIVATE_KEY") - .expect("POLYMARKET_PRIVATE_KEY must be set in .env"); - - let mut client = ClobClient::with_l1_headers( - "https://clob.polymarket.com", - &private_key, - 137 - ); - + + let private_key = + env::var("POLYMARKET_PRIVATE_KEY").expect("POLYMARKET_PRIVATE_KEY must be set in .env"); + + let mut client = ClobClient::with_l1_headers("https://clob.polymarket.com", &private_key, 137); + println!("Step 1: Creating/deriving API key..."); let result = client.create_or_derive_api_key(None).await; - + match result { Ok(creds) => { println!("Successfully created/derived API key"); println!(" API Key: {}", creds.api_key); client.set_api_creds(creds); - + // Now try to get orders (requires auth) println!("\nStep 2: Testing authenticated endpoint (get_orders)..."); let orders_result = client.get_orders(None, None).await; - + match orders_result { Ok(orders) => { println!("Successfully authenticated! Got {} orders", orders.len()); - } + }, Err(e) => { let err_str = format!("{:?}", e); if err_str.contains("401") { @@ -40,12 +36,12 @@ async fn test_create_api_key_simple() { } else { println!("Authentication successful (non-401 error): {:?}", e); } - } + }, } - } + }, Err(e) => { panic!("Failed to create/derive API key: {:?}", e); - } + }, } } @@ -53,27 +49,25 @@ async fn test_create_api_key_simple() { #[ignore] async fn test_get_api_keys() { dotenvy::dotenv().ok(); - - let private_key = env::var("POLYMARKET_PRIVATE_KEY") - .expect("POLYMARKET_PRIVATE_KEY must be set in .env"); - - let mut client = ClobClient::with_l1_headers( - "https://clob.polymarket.com", - &private_key, - 137 - ); - - let creds = client.create_or_derive_api_key(None).await + + let private_key = + env::var("POLYMARKET_PRIVATE_KEY").expect("POLYMARKET_PRIVATE_KEY must be set in .env"); + + let mut client = ClobClient::with_l1_headers("https://clob.polymarket.com", &private_key, 137); + + let creds = client + .create_or_derive_api_key(None) + .await .expect("Failed to create API key"); client.set_api_creds(creds); - + println!("Testing get_api_keys (requires HMAC auth)..."); let result = client.get_api_keys().await; - + match result { Ok(keys) => { println!("Authentication successful! Found {} keys", keys.len()); - } + }, Err(e) => { let err_str = format!("{:?}", e); if err_str.contains("401") { @@ -81,7 +75,7 @@ async fn test_get_api_keys() { } else { panic!("Failed with non-401 error: {:?}", e); } - } + }, } } @@ -89,27 +83,25 @@ async fn test_get_api_keys() { #[ignore] async fn test_get_trades() { dotenvy::dotenv().ok(); - - let private_key = env::var("POLYMARKET_PRIVATE_KEY") - .expect("POLYMARKET_PRIVATE_KEY must be set in .env"); - - let mut client = ClobClient::with_l1_headers( - "https://clob.polymarket.com", - &private_key, - 137 - ); - - let creds = client.create_or_derive_api_key(None).await + + let private_key = + env::var("POLYMARKET_PRIVATE_KEY").expect("POLYMARKET_PRIVATE_KEY must be set in .env"); + + let mut client = ClobClient::with_l1_headers("https://clob.polymarket.com", &private_key, 137); + + let creds = client + .create_or_derive_api_key(None) + .await .expect("Failed to create API key"); client.set_api_creds(creds); - + println!("Testing get_trades (requires HMAC auth)..."); let result = client.get_trades(None, None).await; - + match result { Ok(_) => { println!("Authentication successful!"); - } + }, Err(e) => { let err_str = format!("{:?}", e); if err_str.contains("401") { @@ -117,7 +109,7 @@ async fn test_get_trades() { } else { println!("Authentication successful (got non-401 error): {:?}", e); } - } + }, } } @@ -125,27 +117,25 @@ async fn test_get_trades() { #[ignore] async fn test_get_notifications() { dotenvy::dotenv().ok(); - - let private_key = env::var("POLYMARKET_PRIVATE_KEY") - .expect("POLYMARKET_PRIVATE_KEY must be set in .env"); - - let mut client = ClobClient::with_l1_headers( - "https://clob.polymarket.com", - &private_key, - 137 - ); - - let creds = client.create_or_derive_api_key(None).await + + let private_key = + env::var("POLYMARKET_PRIVATE_KEY").expect("POLYMARKET_PRIVATE_KEY must be set in .env"); + + let mut client = ClobClient::with_l1_headers("https://clob.polymarket.com", &private_key, 137); + + let creds = client + .create_or_derive_api_key(None) + .await .expect("Failed to create API key"); client.set_api_creds(creds); - + println!("Testing get_notifications (requires HMAC auth)..."); let result = client.get_notifications().await; - + match result { Ok(notifs) => { println!("Authentication successful! Notifications: {:?}", notifs); - } + }, Err(e) => { let err_str = format!("{:?}", e); if err_str.contains("401") { @@ -153,7 +143,6 @@ async fn test_get_notifications() { } else { println!("Authentication successful (got non-401 error): {:?}", e); } - } + }, } } -