polyfill_rs/
utils.rs

1//! Utility functions for the Polymarket client
2//!
3//! This module contains optimized utility functions for performance-critical
4//! operations in trading environments.
5
6use crate::errors::{PolyfillError, Result};
7use alloy_primitives::{Address, U256};
8use base64::{engine::general_purpose::URL_SAFE, Engine};
9use chrono::{DateTime, Utc};
10use hmac::{Hmac, Mac};
11use rust_decimal::Decimal;
12use serde::Serialize;
13use sha2::Sha256;
14use std::str::FromStr;
15use std::time::{Duration, SystemTime, UNIX_EPOCH};
16use ::url::Url;
17
18type HmacSha256 = Hmac<Sha256>;
19
20/// High-precision timestamp utilities
21pub mod time {
22    use super::*;
23
24    /// Get current Unix timestamp in seconds
25    #[inline]
26    pub fn now_secs() -> u64 {
27        SystemTime::now()
28            .duration_since(UNIX_EPOCH)
29            .expect("Time went backwards")
30            .as_secs()
31    }
32
33    /// Get current Unix timestamp in milliseconds
34    #[inline]
35    pub fn now_millis() -> u64 {
36        SystemTime::now()
37            .duration_since(UNIX_EPOCH)
38            .expect("Time went backwards")
39            .as_millis() as u64
40    }
41
42    /// Get current Unix timestamp in microseconds
43    #[inline]
44    pub fn now_micros() -> u64 {
45        SystemTime::now()
46            .duration_since(UNIX_EPOCH)
47            .expect("Time went backwards")
48            .as_micros() as u64
49    }
50
51    /// Get current Unix timestamp in nanoseconds
52    #[inline]
53    pub fn now_nanos() -> u128 {
54        SystemTime::now()
55            .duration_since(UNIX_EPOCH)
56            .expect("Time went backwards")
57            .as_nanos()
58    }
59
60    /// Convert DateTime to Unix timestamp in seconds
61    #[inline]
62    pub fn datetime_to_secs(dt: DateTime<Utc>) -> u64 {
63        dt.timestamp() as u64
64    }
65
66    /// Convert Unix timestamp to DateTime
67    #[inline]
68    pub fn secs_to_datetime(timestamp: u64) -> DateTime<Utc> {
69        DateTime::from_timestamp(timestamp as i64, 0)
70            .unwrap_or_else(|| Utc::now())
71    }
72}
73
74/// Cryptographic utilities for signing and authentication
75pub mod crypto {
76    use super::*;
77
78    /// Build HMAC-SHA256 signature for API authentication
79    pub fn build_hmac_signature<T>(
80        secret: &str,
81        timestamp: u64,
82        method: &str,
83        path: &str,
84        body: Option<&T>,
85    ) -> Result<String>
86    where
87        T: ?Sized + Serialize,
88    {
89        let decoded = URL_SAFE
90            .decode(secret)
91            .map_err(|e| PolyfillError::config(format!("Invalid secret format: {}", e)))?;
92
93        let message = match body {
94            None => format!("{timestamp}{method}{path}"),
95            Some(data) => {
96                let json = serde_json::to_string(data)?;
97                format!("{timestamp}{method}{path}{json}")
98            }
99        };
100
101        let mut mac = HmacSha256::new_from_slice(&decoded)
102            .map_err(|e| PolyfillError::internal("HMAC initialization failed", e))?;
103        
104        mac.update(message.as_bytes());
105        let result = mac.finalize();
106
107        Ok(URL_SAFE.encode(result.into_bytes()))
108    }
109
110    /// Generate a secure random nonce
111    pub fn generate_nonce() -> U256 {
112        use rand::RngCore;
113        let mut rng = rand::thread_rng();
114        let mut bytes = [0u8; 32];
115        rng.fill_bytes(&mut bytes);
116        U256::from_be_bytes(bytes)
117    }
118
119    /// Generate a secure random salt
120    pub fn generate_salt() -> u64 {
121        use rand::RngCore;
122        let mut rng = rand::thread_rng();
123        rng.next_u64()
124    }
125}
126
127/// Price and size calculation utilities
128pub mod math {
129    use super::*;
130    use rust_decimal::prelude::*;
131    use crate::types::{Price, Qty, SCALE_FACTOR};
132
133    // ========================================================================
134    // LEGACY DECIMAL FUNCTIONS (for backward compatibility)
135    // ========================================================================
136    // 
137    // These are kept for API compatibility, but internally we should use
138    // the fixed-point versions below for better performance.
139
140    /// Round price to tick size (LEGACY - use fixed-point version when possible)
141    #[inline]
142    pub fn round_to_tick(price: Decimal, tick_size: Decimal) -> Decimal {
143        if tick_size.is_zero() {
144            return price;
145        }
146        (price / tick_size).round() * tick_size
147    }
148
149    /// Calculate notional value (price * size) (LEGACY - use fixed-point version when possible)
150    #[inline]
151    pub fn notional(price: Decimal, size: Decimal) -> Decimal {
152        price * size
153    }
154
155    /// Calculate spread as percentage (LEGACY - use fixed-point version when possible)
156    #[inline]
157    pub fn spread_pct(bid: Decimal, ask: Decimal) -> Option<Decimal> {
158        if bid.is_zero() || ask <= bid {
159            return None;
160        }
161        Some((ask - bid) / bid * Decimal::from(100))
162    }
163
164    /// Calculate mid price (LEGACY - use fixed-point version when possible)
165    #[inline]
166    pub fn mid_price(bid: Decimal, ask: Decimal) -> Option<Decimal> {
167        if bid.is_zero() || ask.is_zero() || ask <= bid {
168            return None;
169        }
170        Some((bid + ask) / Decimal::from(2))
171    }
172
173    // ========================================================================
174    // HIGH-PERFORMANCE FIXED-POINT FUNCTIONS
175    // ========================================================================
176    //
177    // These functions operate on our internal Price/Qty types and are
178    // optimized for maximum performance. They avoid all Decimal operations
179    // and memory allocations.
180    //
181    // Performance comparison (approximate):
182    // - Decimal operations: 20-100ns + allocation overhead
183    // - Fixed-point operations: 1-5ns, no allocations
184    //
185    // That's a 10-50x speedup on the critical path!
186
187    /// Round price to tick size (FAST VERSION)
188    /// 
189    /// This is much faster than the Decimal version because it's just
190    /// integer division and multiplication.
191    /// 
192    /// Example: round_to_tick_fast(6543, 10) = 6540 (rounds to nearest 10 ticks)
193    #[inline]
194    pub fn round_to_tick_fast(price_ticks: Price, tick_size_ticks: Price) -> Price {
195        if tick_size_ticks == 0 {
196            return price_ticks;
197        }
198        // Integer division automatically truncates, then multiply back
199        // For proper rounding, we add half the tick size before dividing
200        let half_tick = tick_size_ticks / 2;
201        ((price_ticks + half_tick) / tick_size_ticks) * tick_size_ticks
202    }
203
204    /// Calculate notional value (price * size) (FAST VERSION)
205    /// 
206    /// Returns the result in the same scale as our quantities.
207    /// This avoids the expensive Decimal multiplication.
208    /// 
209    /// Example: notional_fast(6543, 1000000) = 6543000000 (representing $654.30)
210    #[inline]
211    pub fn notional_fast(price_ticks: Price, size_units: Qty) -> i64 {
212        // Convert price to i64 to avoid overflow
213        let price_i64 = price_ticks as i64;
214        // Multiply and scale appropriately
215        // Both price and size are scaled by SCALE_FACTOR, so result is scaled by SCALE_FACTOR^2
216        // We divide by SCALE_FACTOR to get back to normal scale
217        (price_i64 * size_units) / SCALE_FACTOR
218    }
219
220    /// Calculate spread as percentage (FAST VERSION)
221    /// 
222    /// Returns the spread as a percentage in basis points (1/100th of a percent).
223    /// This avoids floating-point arithmetic entirely.
224    /// 
225    /// Example: spread_pct_fast(6500, 6700) = Some(307) (representing 3.07%)
226    #[inline]
227    pub fn spread_pct_fast(bid_ticks: Price, ask_ticks: Price) -> Option<u32> {
228        if bid_ticks == 0 || ask_ticks <= bid_ticks {
229            return None;
230        }
231        
232        let spread = ask_ticks - bid_ticks;
233        // Calculate percentage in basis points (multiply by 10000 for 4 decimal places)
234        // We use u64 for intermediate calculation to avoid overflow
235        let spread_bps = ((spread as u64) * 10000) / (bid_ticks as u64);
236        
237        // Convert back to u32 (should always fit since spreads are typically small)
238        Some(spread_bps as u32)
239    }
240
241    /// Calculate mid price (FAST VERSION)
242    /// 
243    /// Returns the midpoint between bid and ask in ticks.
244    /// Much faster than the Decimal version.
245    /// 
246    /// Example: mid_price_fast(6500, 6700) = Some(6600)
247    #[inline]
248    pub fn mid_price_fast(bid_ticks: Price, ask_ticks: Price) -> Option<Price> {
249        if bid_ticks == 0 || ask_ticks == 0 || ask_ticks <= bid_ticks {
250            return None;
251        }
252        
253        // Use u64 to avoid overflow in addition
254        let sum = (bid_ticks as u64) + (ask_ticks as u64);
255        Some((sum / 2) as Price)
256    }
257
258    /// Calculate spread in ticks (FAST VERSION)
259    /// 
260    /// Simple subtraction - much faster than Decimal operations.
261    /// 
262    /// Example: spread_fast(6500, 6700) = Some(200) (representing $0.02 spread)
263    #[inline]
264    pub fn spread_fast(bid_ticks: Price, ask_ticks: Price) -> Option<Price> {
265        if ask_ticks <= bid_ticks {
266            return None;
267        }
268        Some(ask_ticks - bid_ticks)
269    }
270
271    /// Check if price is within valid range (FAST VERSION)
272    /// 
273    /// Much faster than converting to Decimal and back.
274    /// 
275    /// Example: is_valid_price_fast(6543, 1, 10000) = true
276    #[inline]
277    pub fn is_valid_price_fast(price_ticks: Price, min_tick: Price, max_tick: Price) -> bool {
278        price_ticks >= min_tick && price_ticks <= max_tick
279    }
280
281    /// Convert decimal to token units (6 decimal places)
282    #[inline]
283    pub fn decimal_to_token_units(amount: Decimal) -> u64 {
284        let scaled = amount * Decimal::from(1_000_000);
285        scaled.to_u64().unwrap_or(0)
286    }
287
288    /// Convert token units back to decimal
289    #[inline]
290    pub fn token_units_to_decimal(units: u64) -> Decimal {
291        Decimal::from(units) / Decimal::from(1_000_000)
292    }
293
294    /// Check if price is within valid range [tick_size, 1-tick_size]
295    #[inline]
296    pub fn is_valid_price(price: Decimal, tick_size: Decimal) -> bool {
297        price >= tick_size && price <= (Decimal::ONE - tick_size)
298    }
299
300    /// Calculate maximum slippage for market order
301    pub fn calculate_slippage(
302        target_price: Decimal,
303        executed_price: Decimal,
304        side: crate::types::Side,
305    ) -> Decimal {
306        match side {
307            crate::types::Side::BUY => {
308                if executed_price > target_price {
309                    (executed_price - target_price) / target_price
310                } else {
311                    Decimal::ZERO
312                }
313            }
314            crate::types::Side::SELL => {
315                if executed_price < target_price {
316                    (target_price - executed_price) / target_price
317                } else {
318                    Decimal::ZERO
319                }
320            }
321        }
322    }
323}
324
325/// Network and retry utilities
326pub mod retry {
327    use super::*;
328    use std::future::Future;
329    use tokio::time::{sleep, Duration};
330
331    /// Exponential backoff configuration
332    #[derive(Debug, Clone)]
333    pub struct RetryConfig {
334        pub max_attempts: usize,
335        pub initial_delay: Duration,
336        pub max_delay: Duration,
337        pub backoff_factor: f64,
338        pub jitter: bool,
339    }
340
341    impl Default for RetryConfig {
342        fn default() -> Self {
343            Self {
344                max_attempts: 3,
345                initial_delay: Duration::from_millis(100),
346                max_delay: Duration::from_secs(10),
347                backoff_factor: 2.0,
348                jitter: true,
349            }
350        }
351    }
352
353    /// Retry a future with exponential backoff
354    pub async fn with_retry<F, Fut, T>(
355        config: &RetryConfig,
356        mut operation: F,
357    ) -> Result<T>
358    where
359        F: FnMut() -> Fut,
360        Fut: Future<Output = Result<T>>,
361    {
362        let mut delay = config.initial_delay;
363        let mut last_error = None;
364
365        for attempt in 0..config.max_attempts {
366            match operation().await {
367                Ok(result) => return Ok(result),
368                Err(err) => {
369                    last_error = Some(err.clone());
370                    
371                    if !err.is_retryable() || attempt == config.max_attempts - 1 {
372                        return Err(err);
373                    }
374
375                    // Add jitter if enabled
376                    let actual_delay = if config.jitter {
377                        let jitter_factor = rand::random::<f64>() * 0.1; // ±10%
378                        let jitter = 1.0 + (jitter_factor - 0.05);
379                        Duration::from_nanos((delay.as_nanos() as f64 * jitter) as u64)
380                    } else {
381                        delay
382                    };
383
384                    sleep(actual_delay).await;
385
386                    // Exponential backoff
387                    delay = std::cmp::min(
388                        Duration::from_nanos((delay.as_nanos() as f64 * config.backoff_factor) as u64),
389                        config.max_delay,
390                    );
391                }
392            }
393        }
394
395        Err(last_error.unwrap_or_else(|| PolyfillError::internal("Retry loop failed", std::io::Error::new(std::io::ErrorKind::Other, "No error captured"))))
396    }
397}
398
399/// Address and token ID utilities
400pub mod address {
401    use super::*;
402
403    /// Validate and parse Ethereum address
404    pub fn parse_address(addr: &str) -> Result<Address> {
405        Address::from_str(addr)
406            .map_err(|e| PolyfillError::validation(format!("Invalid address format: {}", e)))
407    }
408
409    /// Validate token ID format
410    pub fn validate_token_id(token_id: &str) -> Result<()> {
411        if token_id.is_empty() {
412            return Err(PolyfillError::validation("Token ID cannot be empty"));
413        }
414
415        // Token IDs should be numeric strings
416        if !token_id.chars().all(|c| c.is_ascii_digit()) {
417            return Err(PolyfillError::validation("Token ID must be numeric"));
418        }
419
420        Ok(())
421    }
422
423    /// Convert token ID to U256
424    pub fn token_id_to_u256(token_id: &str) -> Result<U256> {
425        validate_token_id(token_id)?;
426        U256::from_str_radix(token_id, 10)
427            .map_err(|e| PolyfillError::validation(format!("Invalid token ID: {}", e)))
428    }
429}
430
431/// URL building utilities
432pub mod url {
433    use super::*;
434
435    /// Build API endpoint URL
436    pub fn build_endpoint(base_url: &str, path: &str) -> Result<String> {
437        let base = base_url.trim_end_matches('/');
438        let path = path.trim_start_matches('/');
439        Ok(format!("{}/{}", base, path))
440    }
441
442    /// Add query parameters to URL
443    pub fn add_query_params(
444        mut url: url::Url,
445        params: &[(&str, &str)],
446    ) -> url::Url {
447        {
448            let mut query_pairs = url.query_pairs_mut();
449            for (key, value) in params {
450                query_pairs.append_pair(key, value);
451            }
452        }
453        url
454    }
455}
456
457/// Rate limiting utilities
458pub mod rate_limit {
459    use super::*;
460    use std::sync::{Arc, Mutex};
461
462    /// Simple token bucket rate limiter
463    #[derive(Debug)]
464    pub struct TokenBucket {
465        capacity: usize,
466        tokens: Arc<Mutex<usize>>,
467        refill_rate: Duration,
468        last_refill: Arc<Mutex<SystemTime>>,
469    }
470
471    impl TokenBucket {
472        pub fn new(capacity: usize, refill_per_second: usize) -> Self {
473            Self {
474                capacity,
475                tokens: Arc::new(Mutex::new(capacity)),
476                refill_rate: Duration::from_secs(1) / refill_per_second as u32,
477                last_refill: Arc::new(Mutex::new(SystemTime::now())),
478            }
479        }
480
481        /// Try to consume a token, return true if successful
482        pub fn try_consume(&self) -> bool {
483            self.refill();
484            
485            let mut tokens = self.tokens.lock().unwrap();
486            if *tokens > 0 {
487                *tokens -= 1;
488                true
489            } else {
490                false
491            }
492        }
493
494        fn refill(&self) {
495            let now = SystemTime::now();
496            let mut last_refill = self.last_refill.lock().unwrap();
497            let elapsed = now.duration_since(*last_refill).unwrap_or_default();
498            
499            if elapsed >= self.refill_rate {
500                let tokens_to_add = elapsed.as_nanos() / self.refill_rate.as_nanos();
501                let mut tokens = self.tokens.lock().unwrap();
502                *tokens = std::cmp::min(self.capacity, *tokens + tokens_to_add as usize);
503                *last_refill = now;
504            }
505        }
506    }
507}
508
509#[cfg(test)]
510mod tests {
511    use super::*;
512
513    #[test]
514    fn test_round_to_tick() {
515        use math::round_to_tick;
516        
517        let price = Decimal::from_str("0.567").unwrap();
518        let tick = Decimal::from_str("0.01").unwrap();
519        let rounded = round_to_tick(price, tick);
520        assert_eq!(rounded, Decimal::from_str("0.57").unwrap());
521    }
522
523    #[test]
524    fn test_mid_price() {
525        use math::mid_price;
526        
527        let bid = Decimal::from_str("0.50").unwrap();
528        let ask = Decimal::from_str("0.52").unwrap();
529        let mid = mid_price(bid, ask).unwrap();
530        assert_eq!(mid, Decimal::from_str("0.51").unwrap());
531    }
532
533    #[test]
534    fn test_token_units_conversion() {
535        use math::{decimal_to_token_units, token_units_to_decimal};
536        
537        let amount = Decimal::from_str("1.234567").unwrap();
538        let units = decimal_to_token_units(amount);
539        assert_eq!(units, 1_234_567);
540        
541        let back = token_units_to_decimal(units);
542        assert_eq!(back, amount);
543    }
544
545    #[test]
546    fn test_address_validation() {
547        use address::parse_address;
548        
549        let valid = "0x1234567890123456789012345678901234567890";
550        assert!(parse_address(valid).is_ok());
551        
552        let invalid = "invalid_address";
553        assert!(parse_address(invalid).is_err());
554    }
555}