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 ::url::Url;
8use alloy_primitives::{Address, U256};
9use base64::{engine::general_purpose::URL_SAFE, Engine};
10use chrono::{DateTime, Utc};
11use hmac::{Hmac, Mac};
12use rust_decimal::Decimal;
13use serde::Serialize;
14use sha2::Sha256;
15use std::str::FromStr;
16use std::time::{Duration, SystemTime, UNIX_EPOCH};
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).unwrap_or_else(Utc::now)
70    }
71}
72
73/// Cryptographic utilities for signing and authentication
74pub mod crypto {
75    use super::*;
76
77    /// Build HMAC-SHA256 signature for API authentication
78    pub fn build_hmac_signature<T>(
79        secret: &str,
80        timestamp: u64,
81        method: &str,
82        path: &str,
83        body: Option<&T>,
84    ) -> Result<String>
85    where
86        T: ?Sized + Serialize,
87    {
88        let decoded = URL_SAFE
89            .decode(secret)
90            .map_err(|e| PolyfillError::config(format!("Invalid secret format: {}", e)))?;
91
92        let message = match body {
93            None => format!("{timestamp}{method}{path}"),
94            Some(data) => {
95                let json = serde_json::to_string(data)?;
96                format!("{timestamp}{method}{path}{json}")
97            },
98        };
99
100        let mut mac = HmacSha256::new_from_slice(&decoded)
101            .map_err(|e| PolyfillError::internal("HMAC initialization failed", e))?;
102
103        mac.update(message.as_bytes());
104        let result = mac.finalize();
105
106        Ok(URL_SAFE.encode(result.into_bytes()))
107    }
108
109    /// Generate a secure random nonce
110    pub fn generate_nonce() -> U256 {
111        use rand::RngCore;
112        let mut rng = rand::thread_rng();
113        let mut bytes = [0u8; 32];
114        rng.fill_bytes(&mut bytes);
115        U256::from_be_bytes(bytes)
116    }
117
118    /// Generate a secure random salt
119    pub fn generate_salt() -> u64 {
120        use rand::RngCore;
121        let mut rng = rand::thread_rng();
122        rng.next_u64()
123    }
124}
125
126/// Price and size calculation utilities
127pub mod math {
128    use super::*;
129    use crate::types::{Price, Qty, SCALE_FACTOR};
130    use rust_decimal::prelude::*;
131
132    // ========================================================================
133    // LEGACY DECIMAL FUNCTIONS (for backward compatibility)
134    // ========================================================================
135    //
136    // These are kept for API compatibility, but internally we should use
137    // the fixed-point versions below for better performance.
138
139    /// Round price to tick size (LEGACY - use fixed-point version when possible)
140    #[inline]
141    pub fn round_to_tick(price: Decimal, tick_size: Decimal) -> Decimal {
142        if tick_size.is_zero() {
143            return price;
144        }
145        (price / tick_size).round() * tick_size
146    }
147
148    /// Calculate notional value (price * size) (LEGACY - use fixed-point version when possible)
149    #[inline]
150    pub fn notional(price: Decimal, size: Decimal) -> Decimal {
151        price * size
152    }
153
154    /// Calculate spread as percentage (LEGACY - use fixed-point version when possible)
155    #[inline]
156    pub fn spread_pct(bid: Decimal, ask: Decimal) -> Option<Decimal> {
157        if bid.is_zero() || ask <= bid {
158            return None;
159        }
160        Some((ask - bid) / bid * Decimal::from(100))
161    }
162
163    /// Calculate mid price (LEGACY - use fixed-point version when possible)
164    #[inline]
165    pub fn mid_price(bid: Decimal, ask: Decimal) -> Option<Decimal> {
166        if bid.is_zero() || ask.is_zero() || ask <= bid {
167            return None;
168        }
169        Some((bid + ask) / Decimal::from(2))
170    }
171
172    // ========================================================================
173    // HIGH-PERFORMANCE FIXED-POINT FUNCTIONS
174    // ========================================================================
175    //
176    // These functions operate on our internal Price/Qty types and are
177    // optimized for maximum performance. They avoid all Decimal operations
178    // and memory allocations.
179    //
180    // Performance comparison (approximate):
181    // - Decimal operations: 20-100ns + allocation overhead
182    // - Fixed-point operations: 1-5ns, no allocations
183    //
184    // That's a 10-50x speedup on the critical path!
185
186    /// Round price to tick size (FAST VERSION)
187    ///
188    /// This is much faster than the Decimal version because it's just
189    /// integer division and multiplication.
190    ///
191    /// Example: round_to_tick_fast(6543, 10) = 6540 (rounds to nearest 10 ticks)
192    #[inline]
193    pub fn round_to_tick_fast(price_ticks: Price, tick_size_ticks: Price) -> Price {
194        if tick_size_ticks == 0 {
195            return price_ticks;
196        }
197        // Integer division automatically truncates, then multiply back
198        // For proper rounding, we add half the tick size before dividing
199        let half_tick = tick_size_ticks / 2;
200        ((price_ticks + half_tick) / tick_size_ticks) * tick_size_ticks
201    }
202
203    /// Calculate notional value (price * size) (FAST VERSION)
204    ///
205    /// Returns the result in the same scale as our quantities.
206    /// This avoids the expensive Decimal multiplication.
207    ///
208    /// Example: notional_fast(6543, 1000000) = 6543000000 (representing $654.30)
209    #[inline]
210    pub fn notional_fast(price_ticks: Price, size_units: Qty) -> i64 {
211        // Convert price to i64 to avoid overflow
212        let price_i64 = price_ticks as i64;
213        // Multiply and scale appropriately
214        // Both price and size are scaled by SCALE_FACTOR, so result is scaled by SCALE_FACTOR^2
215        // We divide by SCALE_FACTOR to get back to normal scale
216        (price_i64 * size_units) / SCALE_FACTOR
217    }
218
219    /// Calculate spread as percentage (FAST VERSION)
220    ///
221    /// Returns the spread as a percentage in basis points (1/100th of a percent).
222    /// This avoids floating-point arithmetic entirely.
223    ///
224    /// Example: spread_pct_fast(6500, 6700) = Some(307) (representing 3.07%)
225    #[inline]
226    pub fn spread_pct_fast(bid_ticks: Price, ask_ticks: Price) -> Option<u32> {
227        if bid_ticks == 0 || ask_ticks <= bid_ticks {
228            return None;
229        }
230
231        let spread = ask_ticks - bid_ticks;
232        // Calculate percentage in basis points (multiply by 10000 for 4 decimal places)
233        // We use u64 for intermediate calculation to avoid overflow
234        let spread_bps = ((spread as u64) * 10000) / (bid_ticks as u64);
235
236        // Convert back to u32 (should always fit since spreads are typically small)
237        Some(spread_bps as u32)
238    }
239
240    /// Calculate mid price (FAST VERSION)
241    ///
242    /// Returns the midpoint between bid and ask in ticks.
243    /// Much faster than the Decimal version.
244    ///
245    /// Example: mid_price_fast(6500, 6700) = Some(6600)
246    #[inline]
247    pub fn mid_price_fast(bid_ticks: Price, ask_ticks: Price) -> Option<Price> {
248        if bid_ticks == 0 || ask_ticks == 0 || ask_ticks <= bid_ticks {
249            return None;
250        }
251
252        // Use u64 to avoid overflow in addition
253        let sum = (bid_ticks as u64) + (ask_ticks as u64);
254        Some((sum / 2) as Price)
255    }
256
257    /// Calculate spread in ticks (FAST VERSION)
258    ///
259    /// Simple subtraction - much faster than Decimal operations.
260    ///
261    /// Example: spread_fast(6500, 6700) = Some(200) (representing $0.02 spread)
262    #[inline]
263    pub fn spread_fast(bid_ticks: Price, ask_ticks: Price) -> Option<Price> {
264        if ask_ticks <= bid_ticks {
265            return None;
266        }
267        Some(ask_ticks - bid_ticks)
268    }
269
270    /// Check if price is within valid range (FAST VERSION)
271    ///
272    /// Much faster than converting to Decimal and back.
273    ///
274    /// Example: is_valid_price_fast(6543, 1, 10000) = true
275    #[inline]
276    pub fn is_valid_price_fast(price_ticks: Price, min_tick: Price, max_tick: Price) -> bool {
277        price_ticks >= min_tick && price_ticks <= max_tick
278    }
279
280    /// Convert decimal to token units (6 decimal places)
281    #[inline]
282    pub fn decimal_to_token_units(amount: Decimal) -> u64 {
283        let scaled = amount * Decimal::from(1_000_000);
284        scaled.to_u64().unwrap_or(0)
285    }
286
287    /// Convert token units back to decimal
288    #[inline]
289    pub fn token_units_to_decimal(units: u64) -> Decimal {
290        Decimal::from(units) / Decimal::from(1_000_000)
291    }
292
293    /// Check if price is within valid range [tick_size, 1-tick_size]
294    #[inline]
295    pub fn is_valid_price(price: Decimal, tick_size: Decimal) -> bool {
296        price >= tick_size && price <= (Decimal::ONE - tick_size)
297    }
298
299    /// Calculate maximum slippage for market order
300    pub fn calculate_slippage(
301        target_price: Decimal,
302        executed_price: Decimal,
303        side: crate::types::Side,
304    ) -> Decimal {
305        match side {
306            crate::types::Side::BUY => {
307                if executed_price > target_price {
308                    (executed_price - target_price) / target_price
309                } else {
310                    Decimal::ZERO
311                }
312            },
313            crate::types::Side::SELL => {
314                if executed_price < target_price {
315                    (target_price - executed_price) / target_price
316                } else {
317                    Decimal::ZERO
318                }
319            },
320        }
321    }
322}
323
324/// Network and retry utilities
325pub mod retry {
326    use super::*;
327    use std::future::Future;
328    use tokio::time::{sleep, Duration};
329
330    /// Exponential backoff configuration
331    #[derive(Debug, Clone)]
332    pub struct RetryConfig {
333        pub max_attempts: usize,
334        pub initial_delay: Duration,
335        pub max_delay: Duration,
336        pub backoff_factor: f64,
337        pub jitter: bool,
338    }
339
340    impl Default for RetryConfig {
341        fn default() -> Self {
342            Self {
343                max_attempts: 3,
344                initial_delay: Duration::from_millis(100),
345                max_delay: Duration::from_secs(10),
346                backoff_factor: 2.0,
347                jitter: true,
348            }
349        }
350    }
351
352    /// Retry a future with exponential backoff
353    pub async fn with_retry<F, Fut, T>(config: &RetryConfig, mut operation: F) -> Result<T>
354    where
355        F: FnMut() -> Fut,
356        Fut: Future<Output = Result<T>>,
357    {
358        let mut delay = config.initial_delay;
359        let mut last_error = None;
360
361        for attempt in 0..config.max_attempts {
362            match operation().await {
363                Ok(result) => return Ok(result),
364                Err(err) => {
365                    last_error = Some(err.clone());
366
367                    if !err.is_retryable() || attempt == config.max_attempts - 1 {
368                        return Err(err);
369                    }
370
371                    // Add jitter if enabled
372                    let actual_delay = if config.jitter {
373                        let jitter_factor = rand::random::<f64>() * 0.1; // ±10%
374                        let jitter = 1.0 + (jitter_factor - 0.05);
375                        Duration::from_nanos((delay.as_nanos() as f64 * jitter) as u64)
376                    } else {
377                        delay
378                    };
379
380                    sleep(actual_delay).await;
381
382                    // Exponential backoff
383                    delay = std::cmp::min(
384                        Duration::from_nanos(
385                            (delay.as_nanos() as f64 * config.backoff_factor) as u64,
386                        ),
387                        config.max_delay,
388                    );
389                },
390            }
391        }
392
393        Err(last_error.unwrap_or_else(|| {
394            PolyfillError::internal(
395                "Retry loop failed",
396                std::io::Error::other("No error captured"),
397            )
398        }))
399    }
400}
401
402/// Address and token ID utilities
403pub mod address {
404    use super::*;
405
406    /// Validate and parse Ethereum address
407    pub fn parse_address(addr: &str) -> Result<Address> {
408        Address::from_str(addr)
409            .map_err(|e| PolyfillError::validation(format!("Invalid address format: {}", e)))
410    }
411
412    /// Validate token ID format
413    pub fn validate_token_id(token_id: &str) -> Result<()> {
414        if token_id.is_empty() {
415            return Err(PolyfillError::validation("Token ID cannot be empty"));
416        }
417
418        // Token IDs should be numeric strings
419        if !token_id.chars().all(|c| c.is_ascii_digit()) {
420            return Err(PolyfillError::validation("Token ID must be numeric"));
421        }
422
423        Ok(())
424    }
425
426    /// Convert token ID to U256
427    pub fn token_id_to_u256(token_id: &str) -> Result<U256> {
428        validate_token_id(token_id)?;
429        U256::from_str_radix(token_id, 10)
430            .map_err(|e| PolyfillError::validation(format!("Invalid token ID: {}", e)))
431    }
432}
433
434/// URL building utilities
435pub mod url {
436    use super::*;
437
438    /// Build API endpoint URL
439    pub fn build_endpoint(base_url: &str, path: &str) -> Result<String> {
440        let base = base_url.trim_end_matches('/');
441        let path = path.trim_start_matches('/');
442        Ok(format!("{}/{}", base, path))
443    }
444
445    /// Add query parameters to URL
446    pub fn add_query_params(mut url: url::Url, params: &[(&str, &str)]) -> 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}