1use 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
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)
70 .unwrap_or_else(|| Utc::now())
71 }
72}
73
74pub mod crypto {
76 use super::*;
77
78 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 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 pub fn generate_salt() -> u64 {
121 use rand::RngCore;
122 let mut rng = rand::thread_rng();
123 rng.next_u64()
124 }
125}
126
127pub mod math {
129 use super::*;
130 use rust_decimal::prelude::*;
131 use crate::types::{Price, Qty, SCALE_FACTOR};
132
133 #[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 #[inline]
151 pub fn notional(price: Decimal, size: Decimal) -> Decimal {
152 price * size
153 }
154
155 #[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 #[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 #[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 let half_tick = tick_size_ticks / 2;
201 ((price_ticks + half_tick) / tick_size_ticks) * tick_size_ticks
202 }
203
204 #[inline]
211 pub fn notional_fast(price_ticks: Price, size_units: Qty) -> i64 {
212 let price_i64 = price_ticks as i64;
214 (price_i64 * size_units) / SCALE_FACTOR
218 }
219
220 #[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 let spread_bps = ((spread as u64) * 10000) / (bid_ticks as u64);
236
237 Some(spread_bps as u32)
239 }
240
241 #[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 let sum = (bid_ticks as u64) + (ask_ticks as u64);
255 Some((sum / 2) as Price)
256 }
257
258 #[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 #[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 #[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 #[inline]
290 pub fn token_units_to_decimal(units: u64) -> Decimal {
291 Decimal::from(units) / Decimal::from(1_000_000)
292 }
293
294 #[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 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
325pub mod retry {
327 use super::*;
328 use std::future::Future;
329 use tokio::time::{sleep, Duration};
330
331 #[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 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 let actual_delay = if config.jitter {
377 let jitter_factor = rand::random::<f64>() * 0.1; 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 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
399pub mod address {
401 use super::*;
402
403 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 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 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 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
431pub mod url {
433 use super::*;
434
435 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 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
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}