1use 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
20pub mod time {
22 use super::*;
23
24 #[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 #[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 #[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 #[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 #[inline]
62 pub fn datetime_to_secs(dt: DateTime<Utc>) -> u64 {
63 dt.timestamp() as u64
64 }
65
66 #[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
73pub mod crypto {
75 use super::*;
76
77 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 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 pub fn generate_salt() -> u64 {
120 use rand::RngCore;
121 let mut rng = rand::thread_rng();
122 rng.next_u64()
123 }
124}
125
126pub mod math {
128 use super::*;
129 use crate::types::{Price, Qty, SCALE_FACTOR};
130 use rust_decimal::prelude::*;
131
132 #[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 #[inline]
150 pub fn notional(price: Decimal, size: Decimal) -> Decimal {
151 price * size
152 }
153
154 #[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 #[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 #[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 let half_tick = tick_size_ticks / 2;
200 ((price_ticks + half_tick) / tick_size_ticks) * tick_size_ticks
201 }
202
203 #[inline]
210 pub fn notional_fast(price_ticks: Price, size_units: Qty) -> i64 {
211 let price_i64 = price_ticks as i64;
213 (price_i64 * size_units) / SCALE_FACTOR
217 }
218
219 #[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 let spread_bps = ((spread as u64) * 10000) / (bid_ticks as u64);
235
236 Some(spread_bps as u32)
238 }
239
240 #[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 let sum = (bid_ticks as u64) + (ask_ticks as u64);
254 Some((sum / 2) as Price)
255 }
256
257 #[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 #[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 #[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 #[inline]
289 pub fn token_units_to_decimal(units: u64) -> Decimal {
290 Decimal::from(units) / Decimal::from(1_000_000)
291 }
292
293 #[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 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
324pub mod retry {
326 use super::*;
327 use std::future::Future;
328 use tokio::time::{sleep, Duration};
329
330 #[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 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 let actual_delay = if config.jitter {
373 let jitter_factor = rand::random::<f64>() * 0.1; 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 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
402pub mod address {
404 use super::*;
405
406 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 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 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 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
434pub mod url {
436 use super::*;
437
438 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 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
457pub mod rate_limit {
459 use super::*;
460 use std::sync::{Arc, Mutex};
461
462 #[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 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}