Skip to main content

polyfill_rs/
client.rs

1//! High-performance Rust client for Polymarket
2//!
3//! This module provides a production-ready client for interacting with
4//! Polymarket, optimized for high-frequency trading environments.
5
6use crate::auth::{create_l1_headers, create_l2_headers};
7use crate::errors::{PolyfillError, Result};
8use crate::http_config::{
9    create_colocated_client, create_internet_client, create_optimized_client, prewarm_connections,
10};
11use crate::types::{OrderOptions, PostOrder, SignedOrderRequest};
12use alloy_primitives::U256;
13use alloy_signer_local::PrivateKeySigner;
14use reqwest::header::HeaderName;
15use reqwest::Client;
16use reqwest::{Method, RequestBuilder};
17use rust_decimal::prelude::FromPrimitive;
18use rust_decimal::Decimal;
19use serde_json::Value;
20use std::str::FromStr;
21
22// Re-export types for compatibility
23pub use crate::types::{ApiCredentials as ApiCreds, OrderType, Side};
24
25// Compatibility types
26#[derive(Debug)]
27pub struct OrderArgs {
28    pub token_id: String,
29    pub price: Decimal,
30    pub size: Decimal,
31    pub side: Side,
32}
33
34impl OrderArgs {
35    pub fn new(token_id: &str, price: Decimal, size: Decimal, side: Side) -> Self {
36        Self {
37            token_id: token_id.to_string(),
38            price,
39            size,
40            side,
41        }
42    }
43}
44
45impl Default for OrderArgs {
46    fn default() -> Self {
47        Self {
48            token_id: "".to_string(),
49            price: Decimal::ZERO,
50            size: Decimal::ZERO,
51            side: Side::BUY,
52        }
53    }
54}
55
56/// Main client for interacting with Polymarket API
57pub struct ClobClient {
58    pub http_client: Client,
59    pub base_url: String,
60    chain_id: u64,
61    signer: Option<PrivateKeySigner>,
62    api_creds: Option<ApiCreds>,
63    order_builder: Option<crate::orders::OrderBuilder>,
64    #[allow(dead_code)]
65    dns_cache: Option<std::sync::Arc<crate::dns_cache::DnsCache>>,
66    #[allow(dead_code)]
67    connection_manager: Option<std::sync::Arc<crate::connection_manager::ConnectionManager>>,
68    #[allow(dead_code)]
69    buffer_pool: std::sync::Arc<crate::buffer_pool::BufferPool>,
70}
71
72impl ClobClient {
73    /// Create a new client with optimized HTTP/2 settings (benchmarked 11.4% faster)
74    /// Now includes DNS caching, connection management, and buffer pooling
75    pub fn new(host: &str) -> Self {
76        // Benchmarked optimal configuration: 512KB stream window
77        // Results: 309.3ms vs 349ms baseline (11.4% improvement)
78        let optimized_client = reqwest::ClientBuilder::new()
79            .http2_adaptive_window(true)
80            .http2_initial_stream_window_size(512 * 1024) // 512KB - empirically optimal
81            .tcp_nodelay(true)
82            .pool_max_idle_per_host(10)
83            .pool_idle_timeout(std::time::Duration::from_secs(90))
84            .build()
85            .unwrap_or_else(|_| Client::new());
86
87        // Initialize DNS cache and pre-warm it
88        let dns_cache = tokio::runtime::Handle::try_current().ok().and_then(|_| {
89            tokio::task::block_in_place(|| {
90                tokio::runtime::Handle::current().block_on(async {
91                    let cache = crate::dns_cache::DnsCache::new().await.ok()?;
92                    let hostname = host
93                        .trim_start_matches("https://")
94                        .trim_start_matches("http://")
95                        .split('/')
96                        .next()?;
97                    cache.prewarm(hostname).await.ok()?;
98                    Some(std::sync::Arc::new(cache))
99                })
100            })
101        });
102
103        // Initialize connection manager
104        let connection_manager = Some(std::sync::Arc::new(
105            crate::connection_manager::ConnectionManager::new(
106                optimized_client.clone(),
107                host.to_string(),
108            ),
109        ));
110
111        // Initialize buffer pool (512KB buffers, pool of 10)
112        let buffer_pool = std::sync::Arc::new(crate::buffer_pool::BufferPool::new(512 * 1024, 10));
113
114        // Pre-warm buffer pool with 3 buffers
115        let pool_clone = buffer_pool.clone();
116        if let Ok(_handle) = tokio::runtime::Handle::try_current() {
117            tokio::spawn(async move {
118                pool_clone.prewarm(3).await;
119            });
120        }
121
122        Self {
123            http_client: optimized_client,
124            base_url: host.to_string(),
125            chain_id: 137, // Default to Polygon
126            signer: None,
127            api_creds: None,
128            order_builder: None,
129            dns_cache,
130            connection_manager,
131            buffer_pool,
132        }
133    }
134
135    /// Create a client optimized for co-located environments
136    pub fn new_colocated(host: &str) -> Self {
137        let http_client = create_colocated_client().unwrap_or_else(|_| Client::new());
138
139        let connection_manager = Some(std::sync::Arc::new(
140            crate::connection_manager::ConnectionManager::new(
141                http_client.clone(),
142                host.to_string(),
143            ),
144        ));
145        let buffer_pool = std::sync::Arc::new(crate::buffer_pool::BufferPool::new(512 * 1024, 10));
146
147        Self {
148            http_client,
149            base_url: host.to_string(),
150            chain_id: 137,
151            signer: None,
152            api_creds: None,
153            order_builder: None,
154            dns_cache: None,
155            connection_manager,
156            buffer_pool,
157        }
158    }
159
160    /// Create a client optimized for internet connections
161    pub fn new_internet(host: &str) -> Self {
162        let http_client = create_internet_client().unwrap_or_else(|_| Client::new());
163
164        let connection_manager = Some(std::sync::Arc::new(
165            crate::connection_manager::ConnectionManager::new(
166                http_client.clone(),
167                host.to_string(),
168            ),
169        ));
170        let buffer_pool = std::sync::Arc::new(crate::buffer_pool::BufferPool::new(512 * 1024, 10));
171
172        Self {
173            http_client,
174            base_url: host.to_string(),
175            chain_id: 137,
176            signer: None,
177            api_creds: None,
178            order_builder: None,
179            dns_cache: None,
180            connection_manager,
181            buffer_pool,
182        }
183    }
184
185    /// Create a client with L1 headers (for authentication)
186    pub fn with_l1_headers(host: &str, private_key: &str, chain_id: u64) -> Self {
187        let signer = private_key
188            .parse::<PrivateKeySigner>()
189            .expect("Invalid private key");
190
191        let order_builder = crate::orders::OrderBuilder::new(signer.clone(), None, None);
192
193        let http_client = create_optimized_client().unwrap_or_else(|_| Client::new());
194
195        // Initialize infrastructure modules
196        let dns_cache = None; // Skip DNS cache for simplicity in this constructor
197        let connection_manager = Some(std::sync::Arc::new(
198            crate::connection_manager::ConnectionManager::new(
199                http_client.clone(),
200                host.to_string(),
201            ),
202        ));
203        let buffer_pool = std::sync::Arc::new(crate::buffer_pool::BufferPool::new(512 * 1024, 10));
204
205        Self {
206            http_client,
207            base_url: host.to_string(),
208            chain_id,
209            signer: Some(signer),
210            api_creds: None,
211            order_builder: Some(order_builder),
212            dns_cache,
213            connection_manager,
214            buffer_pool,
215        }
216    }
217
218    /// Create a client with L2 headers (for API key authentication)
219    pub fn with_l2_headers(
220        host: &str,
221        private_key: &str,
222        chain_id: u64,
223        api_creds: ApiCreds,
224    ) -> Self {
225        let signer = private_key
226            .parse::<PrivateKeySigner>()
227            .expect("Invalid private key");
228
229        let order_builder = crate::orders::OrderBuilder::new(signer.clone(), None, None);
230
231        let http_client = create_optimized_client().unwrap_or_else(|_| Client::new());
232
233        // Initialize infrastructure modules
234        let dns_cache = None; // Skip DNS cache for simplicity in this constructor
235        let connection_manager = Some(std::sync::Arc::new(
236            crate::connection_manager::ConnectionManager::new(
237                http_client.clone(),
238                host.to_string(),
239            ),
240        ));
241        let buffer_pool = std::sync::Arc::new(crate::buffer_pool::BufferPool::new(512 * 1024, 10));
242
243        Self {
244            http_client,
245            base_url: host.to_string(),
246            chain_id,
247            signer: Some(signer),
248            api_creds: Some(api_creds),
249            order_builder: Some(order_builder),
250            dns_cache,
251            connection_manager,
252            buffer_pool,
253        }
254    }
255
256    /// Set API credentials
257    pub fn set_api_creds(&mut self, api_creds: ApiCreds) {
258        self.api_creds = Some(api_creds);
259    }
260
261    /// Start background keep-alive to maintain warm connection
262    /// Sends periodic lightweight requests to prevent connection drops
263    pub async fn start_keepalive(&self, interval: std::time::Duration) {
264        if let Some(manager) = &self.connection_manager {
265            manager.start_keepalive(interval).await;
266        }
267    }
268
269    /// Stop keep-alive background task
270    pub async fn stop_keepalive(&self) {
271        if let Some(manager) = &self.connection_manager {
272            manager.stop_keepalive().await;
273        }
274    }
275
276    /// Pre-warm connections to reduce first-request latency
277    pub async fn prewarm_connections(&self) -> Result<()> {
278        prewarm_connections(&self.http_client, &self.base_url)
279            .await
280            .map_err(|e| {
281                PolyfillError::network(format!("Failed to prewarm connections: {}", e), e)
282            })?;
283        Ok(())
284    }
285
286    /// Get the wallet address
287    pub fn get_address(&self) -> Option<String> {
288        use alloy_primitives::hex;
289        self.signer
290            .as_ref()
291            .map(|s| hex::encode_prefixed(s.address().as_slice()))
292    }
293
294    /// Get the collateral token address for the current chain
295    pub fn get_collateral_address(&self) -> Option<String> {
296        let config = crate::orders::get_contract_config(self.chain_id, false)?;
297        Some(config.collateral)
298    }
299
300    /// Get the conditional tokens contract address for the current chain
301    pub fn get_conditional_address(&self) -> Option<String> {
302        let config = crate::orders::get_contract_config(self.chain_id, false)?;
303        Some(config.conditional_tokens)
304    }
305
306    /// Get the exchange contract address for the current chain
307    pub fn get_exchange_address(&self) -> Option<String> {
308        let config = crate::orders::get_contract_config(self.chain_id, false)?;
309        Some(config.exchange)
310    }
311
312    /// Test basic connectivity
313    pub async fn get_ok(&self) -> bool {
314        match self
315            .http_client
316            .get(format!("{}/ok", self.base_url))
317            .send()
318            .await
319        {
320            Ok(response) => response.status().is_success(),
321            Err(_) => false,
322        }
323    }
324
325    /// Get server time
326    pub async fn get_server_time(&self) -> Result<u64> {
327        let response = self
328            .http_client
329            .get(format!("{}/time", self.base_url))
330            .send()
331            .await?;
332
333        if !response.status().is_success() {
334            return Err(PolyfillError::api(
335                response.status().as_u16(),
336                "Failed to get server time",
337            ));
338        }
339
340        let time_text = response.text().await?;
341        let timestamp = time_text
342            .trim()
343            .parse::<u64>()
344            .map_err(|e| PolyfillError::parse(format!("Invalid timestamp format: {}", e), None))?;
345
346        Ok(timestamp)
347    }
348
349    /// Get order book for a token
350    pub async fn get_order_book(&self, token_id: &str) -> Result<OrderBookSummary> {
351        let response = self
352            .http_client
353            .get(format!("{}/book", self.base_url))
354            .query(&[("token_id", token_id)])
355            .send()
356            .await?;
357
358        if !response.status().is_success() {
359            return Err(PolyfillError::api(
360                response.status().as_u16(),
361                "Failed to get order book",
362            ));
363        }
364
365        let order_book: OrderBookSummary = response.json().await?;
366        Ok(order_book)
367    }
368
369    /// Get midpoint for a token
370    pub async fn get_midpoint(&self, token_id: &str) -> Result<MidpointResponse> {
371        let response = self
372            .http_client
373            .get(format!("{}/midpoint", self.base_url))
374            .query(&[("token_id", token_id)])
375            .send()
376            .await?;
377
378        if !response.status().is_success() {
379            return Err(PolyfillError::api(
380                response.status().as_u16(),
381                "Failed to get midpoint",
382            ));
383        }
384
385        let midpoint: MidpointResponse = response.json().await?;
386        Ok(midpoint)
387    }
388
389    /// Get spread for a token
390    pub async fn get_spread(&self, token_id: &str) -> Result<SpreadResponse> {
391        let response = self
392            .http_client
393            .get(format!("{}/spread", self.base_url))
394            .query(&[("token_id", token_id)])
395            .send()
396            .await?;
397
398        if !response.status().is_success() {
399            return Err(PolyfillError::api(
400                response.status().as_u16(),
401                "Failed to get spread",
402            ));
403        }
404
405        let spread: SpreadResponse = response.json().await?;
406        Ok(spread)
407    }
408
409    /// Get spreads for multiple tokens (batch)
410    pub async fn get_spreads(
411        &self,
412        token_ids: &[String],
413    ) -> Result<std::collections::HashMap<String, Decimal>> {
414        let request_data: Vec<std::collections::HashMap<&str, String>> = token_ids
415            .iter()
416            .map(|id| {
417                let mut map = std::collections::HashMap::new();
418                map.insert("token_id", id.clone());
419                map
420            })
421            .collect();
422
423        let response = self
424            .http_client
425            .post(format!("{}/spreads", self.base_url))
426            .json(&request_data)
427            .send()
428            .await?;
429
430        if !response.status().is_success() {
431            return Err(PolyfillError::api(
432                response.status().as_u16(),
433                "Failed to get batch spreads",
434            ));
435        }
436
437        response
438            .json::<std::collections::HashMap<String, Decimal>>()
439            .await
440            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
441    }
442
443    /// Get price for a token and side
444    pub async fn get_price(&self, token_id: &str, side: Side) -> Result<PriceResponse> {
445        let response = self
446            .http_client
447            .get(format!("{}/price", self.base_url))
448            .query(&[("token_id", token_id), ("side", side.as_str())])
449            .send()
450            .await?;
451
452        if !response.status().is_success() {
453            return Err(PolyfillError::api(
454                response.status().as_u16(),
455                "Failed to get price",
456            ));
457        }
458
459        let price: PriceResponse = response.json().await?;
460        Ok(price)
461    }
462
463    fn validate_prices_history_asset_id(asset_id: &str) -> Result<()> {
464        if asset_id.is_empty() {
465            return Err(PolyfillError::validation(
466                "asset_id is required (use the decimal token_id / asset_id)",
467            ));
468        }
469
470        // Common footgun: passing a condition id (0x...) instead of the decimal asset id.
471        if asset_id.starts_with("0x") || asset_id.starts_with("0X") {
472            return Err(PolyfillError::validation(
473                "`/prices-history` expects a decimal token_id/asset_id, not a hex condition_id",
474            ));
475        }
476
477        if !asset_id.as_bytes().iter().all(u8::is_ascii_digit) {
478            return Err(PolyfillError::validation(
479                "asset_id must be a decimal string (token_id / asset_id)",
480            ));
481        }
482
483        Ok(())
484    }
485
486    /// Get price history for a single outcome (`token_id` / `asset_id`) over a fixed interval.
487    ///
488    /// Important: the upstream API query parameter is named `market`, but it expects the
489    /// decimal outcome asset id (not the hex `condition_id`).
490    pub async fn get_prices_history_interval(
491        &self,
492        asset_id: &str,
493        interval: PricesHistoryInterval,
494        fidelity: Option<u32>,
495    ) -> Result<PricesHistoryResponse> {
496        Self::validate_prices_history_asset_id(asset_id)?;
497
498        let mut request = self
499            .http_client
500            .get(format!("{}/prices-history", self.base_url))
501            .query(&[("market", asset_id), ("interval", interval.as_str())]);
502
503        if let Some(fidelity) = fidelity {
504            request = request.query(&[("fidelity", fidelity)]);
505        }
506
507        let response = request.send().await?;
508        if !response.status().is_success() {
509            let status = response.status().as_u16();
510            let body = response.text().await.unwrap_or_default();
511            let message = serde_json::from_str::<Value>(&body)
512                .ok()
513                .and_then(|v| {
514                    v.get("error")
515                        .and_then(Value::as_str)
516                        .map(|s| s.to_string())
517                })
518                .unwrap_or_else(|| {
519                    if body.is_empty() {
520                        "Failed to get prices history".to_string()
521                    } else {
522                        body
523                    }
524                });
525            return Err(PolyfillError::api(status, message));
526        }
527
528        Ok(response.json::<PricesHistoryResponse>().await?)
529    }
530
531    /// Get price history for a single outcome (`token_id` / `asset_id`) over a timestamp range.
532    ///
533    /// `start_ts` and `end_ts` are Unix timestamps (seconds).
534    pub async fn get_prices_history_range(
535        &self,
536        asset_id: &str,
537        start_ts: u64,
538        end_ts: u64,
539        fidelity: Option<u32>,
540    ) -> Result<PricesHistoryResponse> {
541        Self::validate_prices_history_asset_id(asset_id)?;
542
543        if start_ts >= end_ts {
544            return Err(PolyfillError::validation(
545                "start_ts must be < end_ts for prices history",
546            ));
547        }
548
549        let mut request = self
550            .http_client
551            .get(format!("{}/prices-history", self.base_url))
552            .query(&[("market", asset_id)])
553            .query(&[("startTs", start_ts), ("endTs", end_ts)]);
554
555        if let Some(fidelity) = fidelity {
556            request = request.query(&[("fidelity", fidelity)]);
557        }
558
559        let response = request.send().await?;
560        if !response.status().is_success() {
561            let status = response.status().as_u16();
562            let body = response.text().await.unwrap_or_default();
563            let message = serde_json::from_str::<Value>(&body)
564                .ok()
565                .and_then(|v| {
566                    v.get("error")
567                        .and_then(Value::as_str)
568                        .map(|s| s.to_string())
569                })
570                .unwrap_or_else(|| {
571                    if body.is_empty() {
572                        "Failed to get prices history".to_string()
573                    } else {
574                        body
575                    }
576                });
577            return Err(PolyfillError::api(status, message));
578        }
579
580        Ok(response.json::<PricesHistoryResponse>().await?)
581    }
582
583    /// Get tick size for a token
584    pub async fn get_tick_size(&self, token_id: &str) -> Result<Decimal> {
585        let response = self
586            .http_client
587            .get(format!("{}/tick-size", self.base_url))
588            .query(&[("token_id", token_id)])
589            .send()
590            .await?;
591
592        if !response.status().is_success() {
593            return Err(PolyfillError::api(
594                response.status().as_u16(),
595                "Failed to get tick size",
596            ));
597        }
598
599        let tick_size_response: Value = response.json().await?;
600        let tick_size = tick_size_response["minimum_tick_size"]
601            .as_str()
602            .and_then(|s| Decimal::from_str(s).ok())
603            .or_else(|| {
604                tick_size_response["minimum_tick_size"]
605                    .as_f64()
606                    .map(|f| Decimal::from_f64(f).unwrap_or(Decimal::ZERO))
607            })
608            .ok_or_else(|| PolyfillError::parse("Invalid tick size format", None))?;
609
610        Ok(tick_size)
611    }
612
613    /// Create a new API key
614    pub async fn create_api_key(&self, nonce: Option<U256>) -> Result<ApiCreds> {
615        let signer = self
616            .signer
617            .as_ref()
618            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
619
620        let headers = create_l1_headers(signer, nonce)?;
621        let req =
622            self.create_request_with_headers(Method::POST, "/auth/api-key", headers.into_iter());
623
624        let response = req.send().await?;
625        if !response.status().is_success() {
626            return Err(PolyfillError::api(
627                response.status().as_u16(),
628                "Failed to create API key",
629            ));
630        }
631
632        Ok(response.json::<ApiCreds>().await?)
633    }
634
635    /// Derive an existing API key
636    pub async fn derive_api_key(&self, nonce: Option<U256>) -> Result<ApiCreds> {
637        let signer = self
638            .signer
639            .as_ref()
640            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
641
642        let headers = create_l1_headers(signer, nonce)?;
643        let req = self.create_request_with_headers(
644            Method::GET,
645            "/auth/derive-api-key",
646            headers.into_iter(),
647        );
648
649        let response = req.send().await?;
650        if !response.status().is_success() {
651            return Err(PolyfillError::api(
652                response.status().as_u16(),
653                "Failed to derive API key",
654            ));
655        }
656
657        Ok(response.json::<ApiCreds>().await?)
658    }
659
660    /// Create or derive API key (try create first, fallback to derive)
661    pub async fn create_or_derive_api_key(&self, nonce: Option<U256>) -> Result<ApiCreds> {
662        match self.create_api_key(nonce).await {
663            Ok(creds) => Ok(creds),
664            // Only fall back to derive on API status errors (server responded).
665            // Propagate network/parse/internal errors so callers can handle them appropriately.
666            Err(PolyfillError::Api { .. }) => self.derive_api_key(nonce).await,
667            Err(err) => Err(err),
668        }
669    }
670
671    /// Get all API keys for the authenticated user
672    pub async fn get_api_keys(&self) -> Result<Vec<String>> {
673        let signer = self
674            .signer
675            .as_ref()
676            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
677        let api_creds = self
678            .api_creds
679            .as_ref()
680            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
681
682        let method = Method::GET;
683        let endpoint = "/auth/api-keys";
684        let headers =
685            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
686
687        let response = self
688            .http_client
689            .request(method, format!("{}{}", self.base_url, endpoint))
690            .headers(
691                headers
692                    .into_iter()
693                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
694                    .collect(),
695            )
696            .send()
697            .await
698            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
699
700        let api_keys_response: crate::types::ApiKeysResponse = response
701            .json()
702            .await
703            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?;
704
705        Ok(api_keys_response.api_keys)
706    }
707
708    /// Delete the current API key
709    pub async fn delete_api_key(&self) -> Result<String> {
710        let signer = self
711            .signer
712            .as_ref()
713            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
714        let api_creds = self
715            .api_creds
716            .as_ref()
717            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
718
719        let method = Method::DELETE;
720        let endpoint = "/auth/api-key";
721        let headers =
722            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
723
724        let response = self
725            .http_client
726            .request(method, format!("{}{}", self.base_url, endpoint))
727            .headers(
728                headers
729                    .into_iter()
730                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
731                    .collect(),
732            )
733            .send()
734            .await
735            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
736
737        response
738            .text()
739            .await
740            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
741    }
742
743    /// Helper to create request with headers
744    fn create_request_with_headers(
745        &self,
746        method: Method,
747        endpoint: &str,
748        headers: impl Iterator<Item = (&'static str, String)>,
749    ) -> RequestBuilder {
750        let req = self
751            .http_client
752            .request(method, format!("{}{}", &self.base_url, endpoint));
753        headers.fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v))
754    }
755
756    /// Get neg risk for a token
757    pub async fn get_neg_risk(&self, token_id: &str) -> Result<bool> {
758        let response = self
759            .http_client
760            .get(format!("{}/neg-risk", self.base_url))
761            .query(&[("token_id", token_id)])
762            .send()
763            .await?;
764
765        if !response.status().is_success() {
766            return Err(PolyfillError::api(
767                response.status().as_u16(),
768                "Failed to get neg risk",
769            ));
770        }
771
772        let neg_risk_response: Value = response.json().await?;
773        let neg_risk = neg_risk_response["neg_risk"]
774            .as_bool()
775            .ok_or_else(|| PolyfillError::parse("Invalid neg risk format", None))?;
776
777        Ok(neg_risk)
778    }
779
780    /// Resolve tick size for an order
781    async fn resolve_tick_size(
782        &self,
783        token_id: &str,
784        tick_size: Option<Decimal>,
785    ) -> Result<Decimal> {
786        let min_tick_size = self.get_tick_size(token_id).await?;
787
788        match tick_size {
789            None => Ok(min_tick_size),
790            Some(t) => {
791                if t < min_tick_size {
792                    Err(PolyfillError::validation(format!(
793                        "Tick size {} is smaller than min_tick_size {} for token_id: {}",
794                        t, min_tick_size, token_id
795                    )))
796                } else {
797                    Ok(t)
798                }
799            },
800        }
801    }
802
803    /// Get filled order options
804    async fn get_filled_order_options(
805        &self,
806        token_id: &str,
807        options: Option<&OrderOptions>,
808    ) -> Result<OrderOptions> {
809        let (tick_size, neg_risk, fee_rate_bps) = match options {
810            Some(o) => (o.tick_size, o.neg_risk, o.fee_rate_bps),
811            None => (None, None, None),
812        };
813
814        let tick_size = self.resolve_tick_size(token_id, tick_size).await?;
815        let neg_risk = match neg_risk {
816            Some(nr) => nr,
817            None => self.get_neg_risk(token_id).await?,
818        };
819
820        Ok(OrderOptions {
821            tick_size: Some(tick_size),
822            neg_risk: Some(neg_risk),
823            fee_rate_bps,
824        })
825    }
826
827    /// Check if price is in valid range
828    fn is_price_in_range(&self, price: Decimal, tick_size: Decimal) -> bool {
829        let min_price = tick_size;
830        let max_price = Decimal::ONE - tick_size;
831        price >= min_price && price <= max_price
832    }
833
834    /// Create an order
835    pub async fn create_order(
836        &self,
837        order_args: &OrderArgs,
838        expiration: Option<u64>,
839        extras: Option<crate::types::ExtraOrderArgs>,
840        options: Option<&OrderOptions>,
841    ) -> Result<SignedOrderRequest> {
842        let order_builder = self
843            .order_builder
844            .as_ref()
845            .ok_or_else(|| PolyfillError::auth("Order builder not initialized"))?;
846
847        let create_order_options = self
848            .get_filled_order_options(&order_args.token_id, options)
849            .await?;
850
851        let expiration = expiration.unwrap_or(0);
852        let extras = extras.unwrap_or_default();
853
854        if !self.is_price_in_range(
855            order_args.price,
856            create_order_options.tick_size.expect("Should be filled"),
857        ) {
858            return Err(PolyfillError::validation(
859                "Price is not in range of tick_size",
860            ));
861        }
862
863        order_builder.create_order(
864            self.chain_id,
865            order_args,
866            expiration,
867            &extras,
868            &create_order_options,
869        )
870    }
871
872    /// Calculate market price from order book
873    async fn calculate_market_price(
874        &self,
875        token_id: &str,
876        side: Side,
877        amount: Decimal,
878    ) -> Result<Decimal> {
879        let book = self.get_order_book(token_id).await?;
880        let order_builder = self
881            .order_builder
882            .as_ref()
883            .ok_or_else(|| PolyfillError::auth("Order builder not initialized"))?;
884
885        // Convert OrderSummary to BookLevel
886        let levels: Vec<crate::types::BookLevel> = match side {
887            Side::BUY => book
888                .asks
889                .into_iter()
890                .map(|s| crate::types::BookLevel {
891                    price: s.price,
892                    size: s.size,
893                })
894                .collect(),
895            Side::SELL => book
896                .bids
897                .into_iter()
898                .map(|s| crate::types::BookLevel {
899                    price: s.price,
900                    size: s.size,
901                })
902                .collect(),
903        };
904
905        order_builder.calculate_market_price(&levels, amount)
906    }
907
908    /// Create a market order
909    pub async fn create_market_order(
910        &self,
911        order_args: &crate::types::MarketOrderArgs,
912        extras: Option<crate::types::ExtraOrderArgs>,
913        options: Option<&OrderOptions>,
914    ) -> Result<SignedOrderRequest> {
915        let order_builder = self
916            .order_builder
917            .as_ref()
918            .ok_or_else(|| PolyfillError::auth("Order builder not initialized"))?;
919
920        let create_order_options = self
921            .get_filled_order_options(&order_args.token_id, options)
922            .await?;
923
924        let extras = extras.unwrap_or_default();
925        let price = self
926            .calculate_market_price(&order_args.token_id, Side::BUY, order_args.amount)
927            .await?;
928
929        if !self.is_price_in_range(
930            price,
931            create_order_options.tick_size.expect("Should be filled"),
932        ) {
933            return Err(PolyfillError::validation(
934                "Price is not in range of tick_size",
935            ));
936        }
937
938        order_builder.create_market_order(
939            self.chain_id,
940            order_args,
941            price,
942            &extras,
943            &create_order_options,
944        )
945    }
946
947    /// Post an order to the exchange
948    pub async fn post_order(
949        &self,
950        order: SignedOrderRequest,
951        order_type: OrderType,
952    ) -> Result<Value> {
953        let signer = self
954            .signer
955            .as_ref()
956            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
957        let api_creds = self
958            .api_creds
959            .as_ref()
960            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
961
962        // Owner field must reference the credential principal identifier
963        // to maintain consistency with the authentication context layer
964        let body = PostOrder::new(order, api_creds.api_key.clone(), order_type);
965
966        let headers = create_l2_headers(signer, api_creds, "POST", "/order", Some(&body))?;
967        let req = self.create_request_with_headers(Method::POST, "/order", headers.into_iter());
968
969        let response = req.json(&body).send().await?;
970        if !response.status().is_success() {
971            let status = response.status().as_u16();
972            let body = response.text().await.unwrap_or_default();
973            let message = if body.is_empty() {
974                "Failed to post order".to_string()
975            } else {
976                format!("Failed to post order: {}", body)
977            };
978            return Err(PolyfillError::api(status, message));
979        }
980
981        Ok(response.json::<Value>().await?)
982    }
983
984    /// Create and post an order in one call
985    pub async fn create_and_post_order(&self, order_args: &OrderArgs) -> Result<Value> {
986        let order = self.create_order(order_args, None, None, None).await?;
987        self.post_order(order, OrderType::GTC).await
988    }
989
990    /// Cancel an order
991    pub async fn cancel(&self, order_id: &str) -> Result<Value> {
992        let signer = self
993            .signer
994            .as_ref()
995            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
996        let api_creds = self
997            .api_creds
998            .as_ref()
999            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1000
1001        let body = std::collections::HashMap::from([("orderID", order_id)]);
1002
1003        let headers = create_l2_headers(signer, api_creds, "DELETE", "/order", Some(&body))?;
1004        let req = self.create_request_with_headers(Method::DELETE, "/order", headers.into_iter());
1005
1006        let response = req.json(&body).send().await?;
1007        if !response.status().is_success() {
1008            return Err(PolyfillError::api(
1009                response.status().as_u16(),
1010                "Failed to cancel order",
1011            ));
1012        }
1013
1014        Ok(response.json::<Value>().await?)
1015    }
1016
1017    /// Cancel multiple orders
1018    pub async fn cancel_orders(&self, order_ids: &[String]) -> Result<Value> {
1019        let signer = self
1020            .signer
1021            .as_ref()
1022            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1023        let api_creds = self
1024            .api_creds
1025            .as_ref()
1026            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1027
1028        let headers = create_l2_headers(signer, api_creds, "DELETE", "/orders", Some(order_ids))?;
1029        let req = self.create_request_with_headers(Method::DELETE, "/orders", headers.into_iter());
1030
1031        let response = req.json(order_ids).send().await?;
1032        if !response.status().is_success() {
1033            return Err(PolyfillError::api(
1034                response.status().as_u16(),
1035                "Failed to cancel orders",
1036            ));
1037        }
1038
1039        Ok(response.json::<Value>().await?)
1040    }
1041
1042    /// Cancel all orders
1043    pub async fn cancel_all(&self) -> Result<Value> {
1044        let signer = self
1045            .signer
1046            .as_ref()
1047            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1048        let api_creds = self
1049            .api_creds
1050            .as_ref()
1051            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1052
1053        let headers = create_l2_headers::<Value>(signer, api_creds, "DELETE", "/cancel-all", None)?;
1054        let req =
1055            self.create_request_with_headers(Method::DELETE, "/cancel-all", headers.into_iter());
1056
1057        let response = req.send().await?;
1058        if !response.status().is_success() {
1059            return Err(PolyfillError::api(
1060                response.status().as_u16(),
1061                "Failed to cancel all orders",
1062            ));
1063        }
1064
1065        Ok(response.json::<Value>().await?)
1066    }
1067
1068    /// Get open orders with optional filtering
1069    ///
1070    /// This retrieves all open orders for the authenticated user. You can filter by:
1071    /// - Order ID (exact match)
1072    /// - Asset/Token ID (all orders for a specific token)
1073    /// - Market ID (all orders for a specific market)
1074    ///
1075    /// The response includes order status, fill information, and timestamps.
1076    pub async fn get_orders(
1077        &self,
1078        params: Option<&crate::types::OpenOrderParams>,
1079        next_cursor: Option<&str>,
1080    ) -> Result<Vec<crate::types::OpenOrder>> {
1081        let signer = self
1082            .signer
1083            .as_ref()
1084            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1085        let api_creds = self
1086            .api_creds
1087            .as_ref()
1088            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1089
1090        let method = Method::GET;
1091        let endpoint = "/data/orders";
1092        let headers =
1093            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1094
1095        let query_params = match params {
1096            None => Vec::new(),
1097            Some(p) => p.to_query_params(),
1098        };
1099
1100        let mut next_cursor = next_cursor.unwrap_or("MA==").to_string(); // INITIAL_CURSOR
1101        let mut output = Vec::new();
1102
1103        while next_cursor != "LTE=" {
1104            // END_CURSOR
1105            let req = self
1106                .http_client
1107                .request(method.clone(), format!("{}{}", self.base_url, endpoint))
1108                .query(&query_params)
1109                .query(&[("next_cursor", &next_cursor)]);
1110
1111            let r = headers
1112                .clone()
1113                .into_iter()
1114                .fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v));
1115
1116            let resp = r
1117                .send()
1118                .await
1119                .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?
1120                .json::<Value>()
1121                .await
1122                .map_err(|e| {
1123                    PolyfillError::parse(format!("Failed to parse response: {}", e), None)
1124                })?;
1125
1126            let new_cursor = resp["next_cursor"]
1127                .as_str()
1128                .ok_or_else(|| {
1129                    PolyfillError::parse("Failed to parse next cursor".to_string(), None)
1130                })?
1131                .to_owned();
1132
1133            next_cursor = new_cursor;
1134
1135            let results = resp["data"].clone();
1136            let orders =
1137                serde_json::from_value::<Vec<crate::types::OpenOrder>>(results).map_err(|e| {
1138                    PolyfillError::parse(
1139                        format!("Failed to parse data from order response: {}", e),
1140                        None,
1141                    )
1142                })?;
1143            output.extend(orders);
1144        }
1145
1146        Ok(output)
1147    }
1148
1149    /// Get trade history with optional filtering
1150    ///
1151    /// This retrieves historical trades for the authenticated user. You can filter by:
1152    /// - Trade ID (exact match)
1153    /// - Maker address (trades where you were the maker)
1154    /// - Market ID (trades in a specific market)
1155    /// - Asset/Token ID (trades for a specific token)
1156    /// - Time range (before/after timestamps)
1157    ///
1158    /// Trades are returned in reverse chronological order (newest first).
1159    pub async fn get_trades(
1160        &self,
1161        trade_params: Option<&crate::types::TradeParams>,
1162        next_cursor: Option<&str>,
1163    ) -> Result<Vec<Value>> {
1164        let signer = self
1165            .signer
1166            .as_ref()
1167            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1168        let api_creds = self
1169            .api_creds
1170            .as_ref()
1171            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1172
1173        let method = Method::GET;
1174        let endpoint = "/data/trades";
1175        let headers =
1176            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1177
1178        let query_params = match trade_params {
1179            None => Vec::new(),
1180            Some(p) => p.to_query_params(),
1181        };
1182
1183        let mut next_cursor = next_cursor.unwrap_or("MA==").to_string(); // INITIAL_CURSOR
1184        let mut output = Vec::new();
1185
1186        while next_cursor != "LTE=" {
1187            // END_CURSOR
1188            let req = self
1189                .http_client
1190                .request(method.clone(), format!("{}{}", self.base_url, endpoint))
1191                .query(&query_params)
1192                .query(&[("next_cursor", &next_cursor)]);
1193
1194            let r = headers
1195                .clone()
1196                .into_iter()
1197                .fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v));
1198
1199            let resp = r
1200                .send()
1201                .await
1202                .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?
1203                .json::<Value>()
1204                .await
1205                .map_err(|e| {
1206                    PolyfillError::parse(format!("Failed to parse response: {}", e), None)
1207                })?;
1208
1209            let new_cursor = resp["next_cursor"]
1210                .as_str()
1211                .ok_or_else(|| {
1212                    PolyfillError::parse("Failed to parse next cursor".to_string(), None)
1213                })?
1214                .to_owned();
1215
1216            next_cursor = new_cursor;
1217
1218            let results = resp["data"].clone();
1219            output.push(results);
1220        }
1221
1222        Ok(output)
1223    }
1224
1225    /// Get balance and allowance information for all assets
1226    ///
1227    /// This returns the current balance and allowance for each asset in your account.
1228    /// Balance is how much you own, allowance is how much the exchange can spend on your behalf.
1229    ///
1230    /// You need both balance and allowance to place orders - the exchange needs permission
1231    /// to move your tokens when orders are filled.
1232    pub async fn get_balance_allowance(
1233        &self,
1234        params: Option<crate::types::BalanceAllowanceParams>,
1235    ) -> Result<Value> {
1236        let signer = self
1237            .signer
1238            .as_ref()
1239            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1240        let api_creds = self
1241            .api_creds
1242            .as_ref()
1243            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1244
1245        let mut params = params.unwrap_or_default();
1246        if params.signature_type.is_none() {
1247            params.set_signature_type(
1248                self.order_builder
1249                    .as_ref()
1250                    .expect("OrderBuilder not set")
1251                    .get_sig_type(),
1252            );
1253        }
1254
1255        let query_params = params.to_query_params();
1256
1257        let method = Method::GET;
1258        let endpoint = "/balance-allowance";
1259        let headers =
1260            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1261
1262        let response = self
1263            .http_client
1264            .request(method, format!("{}{}", self.base_url, endpoint))
1265            .headers(
1266                headers
1267                    .into_iter()
1268                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1269                    .collect(),
1270            )
1271            .query(&query_params)
1272            .send()
1273            .await
1274            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1275
1276        response
1277            .json::<Value>()
1278            .await
1279            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1280    }
1281
1282    /// Set up notifications for order fills and other events
1283    ///
1284    /// This configures push notifications so you get alerted when:
1285    /// - Your orders get filled
1286    /// - Your orders get cancelled
1287    /// - Market conditions change significantly
1288    ///
1289    /// The signature proves you own the account and want to receive notifications.
1290    pub async fn get_notifications(&self) -> Result<Value> {
1291        let signer = self
1292            .signer
1293            .as_ref()
1294            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1295        let api_creds = self
1296            .api_creds
1297            .as_ref()
1298            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1299
1300        let method = Method::GET;
1301        let endpoint = "/notifications";
1302        let headers =
1303            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1304
1305        let response = self
1306            .http_client
1307            .request(method, format!("{}{}", self.base_url, endpoint))
1308            .headers(
1309                headers
1310                    .into_iter()
1311                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1312                    .collect(),
1313            )
1314            .query(&[(
1315                "signature_type",
1316                &self
1317                    .order_builder
1318                    .as_ref()
1319                    .expect("OrderBuilder not set")
1320                    .get_sig_type()
1321                    .to_string(),
1322            )])
1323            .send()
1324            .await
1325            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1326
1327        response
1328            .json::<Value>()
1329            .await
1330            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1331    }
1332
1333    /// Get midpoints for multiple tokens in a single request
1334    ///
1335    /// This is much more efficient than calling get_midpoint() multiple times.
1336    /// Instead of N round trips, you make just 1 request and get all the midpoints back.
1337    ///
1338    /// Midpoints are returned as a HashMap where the key is the token_id and the value
1339    /// is the midpoint price (or None if there's no valid midpoint).
1340    pub async fn get_midpoints(
1341        &self,
1342        token_ids: &[String],
1343    ) -> Result<std::collections::HashMap<String, Decimal>> {
1344        let request_data: Vec<std::collections::HashMap<&str, String>> = token_ids
1345            .iter()
1346            .map(|id| {
1347                let mut map = std::collections::HashMap::new();
1348                map.insert("token_id", id.clone());
1349                map
1350            })
1351            .collect();
1352
1353        let response = self
1354            .http_client
1355            .post(format!("{}/midpoints", self.base_url))
1356            .json(&request_data)
1357            .send()
1358            .await?;
1359
1360        if !response.status().is_success() {
1361            return Err(PolyfillError::api(
1362                response.status().as_u16(),
1363                "Failed to get batch midpoints",
1364            ));
1365        }
1366
1367        let midpoints: std::collections::HashMap<String, Decimal> = response.json().await?;
1368        Ok(midpoints)
1369    }
1370
1371    /// Get bid/ask/mid prices for multiple tokens in a single request
1372    ///
1373    /// This gives you the full price picture for multiple tokens at once.
1374    /// Much more efficient than individual calls, especially when you're tracking
1375    /// a portfolio or comparing multiple markets.
1376    ///
1377    /// Returns bid (best buy price), ask (best sell price), and mid (average) for each token.
1378    pub async fn get_prices(
1379        &self,
1380        book_params: &[crate::types::BookParams],
1381    ) -> Result<std::collections::HashMap<String, std::collections::HashMap<Side, Decimal>>> {
1382        let request_data: Vec<std::collections::HashMap<&str, String>> = book_params
1383            .iter()
1384            .map(|params| {
1385                let mut map = std::collections::HashMap::new();
1386                map.insert("token_id", params.token_id.clone());
1387                map.insert("side", params.side.as_str().to_string());
1388                map
1389            })
1390            .collect();
1391
1392        let response = self
1393            .http_client
1394            .post(format!("{}/prices", self.base_url))
1395            .json(&request_data)
1396            .send()
1397            .await?;
1398
1399        if !response.status().is_success() {
1400            return Err(PolyfillError::api(
1401                response.status().as_u16(),
1402                "Failed to get batch prices",
1403            ));
1404        }
1405
1406        let prices: std::collections::HashMap<String, std::collections::HashMap<Side, Decimal>> =
1407            response.json().await?;
1408        Ok(prices)
1409    }
1410
1411    /// Get order book for multiple tokens (batch) - reference implementation compatible
1412    pub async fn get_order_books(&self, token_ids: &[String]) -> Result<Vec<OrderBookSummary>> {
1413        let request_data: Vec<std::collections::HashMap<&str, String>> = token_ids
1414            .iter()
1415            .map(|id| {
1416                let mut map = std::collections::HashMap::new();
1417                map.insert("token_id", id.clone());
1418                map
1419            })
1420            .collect();
1421
1422        let response = self
1423            .http_client
1424            .post(format!("{}/books", self.base_url))
1425            .json(&request_data)
1426            .send()
1427            .await
1428            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1429
1430        response
1431            .json::<Vec<OrderBookSummary>>()
1432            .await
1433            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1434    }
1435
1436    /// Get single order by ID
1437    pub async fn get_order(&self, order_id: &str) -> Result<crate::types::OpenOrder> {
1438        let signer = self
1439            .signer
1440            .as_ref()
1441            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1442        let api_creds = self
1443            .api_creds
1444            .as_ref()
1445            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1446
1447        let method = Method::GET;
1448        let endpoint = &format!("/data/order/{}", order_id);
1449        let headers =
1450            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1451
1452        let response = self
1453            .http_client
1454            .request(method, format!("{}{}", self.base_url, endpoint))
1455            .headers(
1456                headers
1457                    .into_iter()
1458                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1459                    .collect(),
1460            )
1461            .send()
1462            .await
1463            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1464
1465        response
1466            .json::<crate::types::OpenOrder>()
1467            .await
1468            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1469    }
1470
1471    /// Get last trade price for a token
1472    pub async fn get_last_trade_price(&self, token_id: &str) -> Result<Value> {
1473        let response = self
1474            .http_client
1475            .get(format!("{}/last-trade-price", self.base_url))
1476            .query(&[("token_id", token_id)])
1477            .send()
1478            .await
1479            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1480
1481        response
1482            .json::<Value>()
1483            .await
1484            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1485    }
1486
1487    /// Get last trade prices for multiple tokens
1488    pub async fn get_last_trade_prices(&self, token_ids: &[String]) -> Result<Value> {
1489        let request_data: Vec<std::collections::HashMap<&str, String>> = token_ids
1490            .iter()
1491            .map(|id| {
1492                let mut map = std::collections::HashMap::new();
1493                map.insert("token_id", id.clone());
1494                map
1495            })
1496            .collect();
1497
1498        let response = self
1499            .http_client
1500            .post(format!("{}/last-trades-prices", self.base_url))
1501            .json(&request_data)
1502            .send()
1503            .await
1504            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1505
1506        response
1507            .json::<Value>()
1508            .await
1509            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1510    }
1511
1512    /// Cancel market orders with optional filters
1513    pub async fn cancel_market_orders(
1514        &self,
1515        market: Option<&str>,
1516        asset_id: Option<&str>,
1517    ) -> Result<Value> {
1518        let signer = self
1519            .signer
1520            .as_ref()
1521            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1522        let api_creds = self
1523            .api_creds
1524            .as_ref()
1525            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1526
1527        let method = Method::DELETE;
1528        let endpoint = "/cancel-market-orders";
1529        let body = std::collections::HashMap::from([
1530            ("market", market.unwrap_or("")),
1531            ("asset_id", asset_id.unwrap_or("")),
1532        ]);
1533
1534        let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(&body))?;
1535
1536        let response = self
1537            .http_client
1538            .request(method, format!("{}{}", self.base_url, endpoint))
1539            .headers(
1540                headers
1541                    .into_iter()
1542                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1543                    .collect(),
1544            )
1545            .json(&body)
1546            .send()
1547            .await
1548            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1549
1550        response
1551            .json::<Value>()
1552            .await
1553            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1554    }
1555
1556    /// Drop (delete) notifications by IDs
1557    pub async fn drop_notifications(&self, ids: &[String]) -> Result<Value> {
1558        let signer = self
1559            .signer
1560            .as_ref()
1561            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1562        let api_creds = self
1563            .api_creds
1564            .as_ref()
1565            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1566
1567        let method = Method::DELETE;
1568        let endpoint = "/notifications";
1569        let headers =
1570            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1571
1572        let response = self
1573            .http_client
1574            .request(method, format!("{}{}", self.base_url, endpoint))
1575            .headers(
1576                headers
1577                    .into_iter()
1578                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1579                    .collect(),
1580            )
1581            .query(&[("ids", ids.join(","))])
1582            .send()
1583            .await
1584            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1585
1586        response
1587            .json::<Value>()
1588            .await
1589            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1590    }
1591
1592    /// Update balance allowance
1593    pub async fn update_balance_allowance(
1594        &self,
1595        params: Option<crate::types::BalanceAllowanceParams>,
1596    ) -> Result<Value> {
1597        let signer = self
1598            .signer
1599            .as_ref()
1600            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1601        let api_creds = self
1602            .api_creds
1603            .as_ref()
1604            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1605
1606        let mut params = params.unwrap_or_default();
1607        if params.signature_type.is_none() {
1608            params.set_signature_type(
1609                self.order_builder
1610                    .as_ref()
1611                    .expect("OrderBuilder not set")
1612                    .get_sig_type(),
1613            );
1614        }
1615
1616        let query_params = params.to_query_params();
1617
1618        let method = Method::GET;
1619        let endpoint = "/balance-allowance/update";
1620        let headers =
1621            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1622
1623        let response = self
1624            .http_client
1625            .request(method, format!("{}{}", self.base_url, endpoint))
1626            .headers(
1627                headers
1628                    .into_iter()
1629                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1630                    .collect(),
1631            )
1632            .query(&query_params)
1633            .send()
1634            .await
1635            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1636
1637        response
1638            .json::<Value>()
1639            .await
1640            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1641    }
1642
1643    /// Check if an order is scoring
1644    pub async fn is_order_scoring(&self, order_id: &str) -> Result<bool> {
1645        let signer = self
1646            .signer
1647            .as_ref()
1648            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1649        let api_creds = self
1650            .api_creds
1651            .as_ref()
1652            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1653
1654        let method = Method::GET;
1655        let endpoint = "/order-scoring";
1656        let headers =
1657            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1658
1659        let response = self
1660            .http_client
1661            .request(method, format!("{}{}", self.base_url, endpoint))
1662            .headers(
1663                headers
1664                    .into_iter()
1665                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1666                    .collect(),
1667            )
1668            .query(&[("order_id", order_id)])
1669            .send()
1670            .await
1671            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1672
1673        let result: Value = response
1674            .json()
1675            .await
1676            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?;
1677
1678        Ok(result["scoring"].as_bool().unwrap_or(false))
1679    }
1680
1681    /// Check if multiple orders are scoring
1682    pub async fn are_orders_scoring(
1683        &self,
1684        order_ids: &[&str],
1685    ) -> Result<std::collections::HashMap<String, bool>> {
1686        let signer = self
1687            .signer
1688            .as_ref()
1689            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1690        let api_creds = self
1691            .api_creds
1692            .as_ref()
1693            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1694
1695        let method = Method::POST;
1696        let endpoint = "/orders-scoring";
1697        let headers = create_l2_headers(
1698            signer,
1699            api_creds,
1700            method.as_str(),
1701            endpoint,
1702            Some(order_ids),
1703        )?;
1704
1705        let response = self
1706            .http_client
1707            .request(method, format!("{}{}", self.base_url, endpoint))
1708            .headers(
1709                headers
1710                    .into_iter()
1711                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1712                    .collect(),
1713            )
1714            .json(order_ids)
1715            .send()
1716            .await
1717            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1718
1719        response
1720            .json::<std::collections::HashMap<String, bool>>()
1721            .await
1722            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1723    }
1724
1725    /// Get sampling markets with pagination
1726    pub async fn get_sampling_markets(
1727        &self,
1728        next_cursor: Option<&str>,
1729    ) -> Result<crate::types::MarketsResponse> {
1730        let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR
1731
1732        let response = self
1733            .http_client
1734            .get(format!("{}/sampling-markets", self.base_url))
1735            .query(&[("next_cursor", next_cursor)])
1736            .send()
1737            .await
1738            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1739
1740        response
1741            .json::<crate::types::MarketsResponse>()
1742            .await
1743            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1744    }
1745
1746    /// Get sampling simplified markets with pagination
1747    pub async fn get_sampling_simplified_markets(
1748        &self,
1749        next_cursor: Option<&str>,
1750    ) -> Result<crate::types::SimplifiedMarketsResponse> {
1751        let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR
1752
1753        let response = self
1754            .http_client
1755            .get(format!("{}/sampling-simplified-markets", self.base_url))
1756            .query(&[("next_cursor", next_cursor)])
1757            .send()
1758            .await
1759            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1760
1761        response
1762            .json::<crate::types::SimplifiedMarketsResponse>()
1763            .await
1764            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1765    }
1766
1767    /// Get markets with pagination
1768    pub async fn get_markets(
1769        &self,
1770        next_cursor: Option<&str>,
1771    ) -> Result<crate::types::MarketsResponse> {
1772        let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR
1773
1774        let response = self
1775            .http_client
1776            .get(format!("{}/markets", self.base_url))
1777            .query(&[("next_cursor", next_cursor)])
1778            .send()
1779            .await
1780            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1781
1782        response
1783            .json::<crate::types::MarketsResponse>()
1784            .await
1785            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1786    }
1787
1788    /// Get simplified markets with pagination
1789    pub async fn get_simplified_markets(
1790        &self,
1791        next_cursor: Option<&str>,
1792    ) -> Result<crate::types::SimplifiedMarketsResponse> {
1793        let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR
1794
1795        let response = self
1796            .http_client
1797            .get(format!("{}/simplified-markets", self.base_url))
1798            .query(&[("next_cursor", next_cursor)])
1799            .send()
1800            .await
1801            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1802
1803        response
1804            .json::<crate::types::SimplifiedMarketsResponse>()
1805            .await
1806            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1807    }
1808
1809    /// Get single market by condition ID
1810    pub async fn get_market(&self, condition_id: &str) -> Result<crate::types::Market> {
1811        let response = self
1812            .http_client
1813            .get(format!("{}/markets/{}", self.base_url, condition_id))
1814            .send()
1815            .await
1816            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1817
1818        response
1819            .json::<crate::types::Market>()
1820            .await
1821            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1822    }
1823
1824    /// Get market trades events
1825    pub async fn get_market_trades_events(&self, condition_id: &str) -> Result<Value> {
1826        let response = self
1827            .http_client
1828            .get(format!(
1829                "{}/live-activity/events/{}",
1830                self.base_url, condition_id
1831            ))
1832            .send()
1833            .await
1834            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1835
1836        response
1837            .json::<Value>()
1838            .await
1839            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1840    }
1841}
1842
1843// Re-export types from the canonical location in types.rs
1844pub use crate::types::{
1845    ExtraOrderArgs, Market, MarketOrderArgs, MarketsResponse, MidpointResponse, NegRiskResponse,
1846    OrderBookSummary, OrderSummary, PriceResponse, PricesHistoryInterval, PricesHistoryResponse,
1847    Rewards, SpreadResponse, TickSizeResponse, Token,
1848};
1849
1850// Compatibility types that need to stay in client.rs
1851#[derive(Debug, Default)]
1852pub struct CreateOrderOptions {
1853    pub tick_size: Option<Decimal>,
1854    pub neg_risk: Option<bool>,
1855}
1856
1857// Re-export for compatibility
1858pub type PolyfillClient = ClobClient;
1859
1860#[cfg(test)]
1861mod tests {
1862    use super::{ClobClient, OrderArgs as ClientOrderArgs};
1863    use crate::types::{PricesHistoryInterval, Side};
1864    use crate::{ApiCredentials, PolyfillError};
1865    use mockito::{Matcher, Server};
1866    use rust_decimal::Decimal;
1867    use std::str::FromStr;
1868    use tokio;
1869
1870    fn create_test_client(base_url: &str) -> ClobClient {
1871        ClobClient::new(base_url)
1872    }
1873
1874    fn create_test_client_with_auth(base_url: &str) -> ClobClient {
1875        ClobClient::with_l1_headers(
1876            base_url,
1877            "0x1234567890123456789012345678901234567890123456789012345678901234",
1878            137,
1879        )
1880    }
1881
1882    #[tokio::test(flavor = "multi_thread")]
1883    async fn test_client_creation() {
1884        let client = create_test_client("https://test.example.com");
1885        assert_eq!(client.base_url, "https://test.example.com");
1886        assert!(client.signer.is_none());
1887        assert!(client.api_creds.is_none());
1888    }
1889
1890    #[tokio::test(flavor = "multi_thread")]
1891    async fn test_client_with_l1_headers() {
1892        let client = create_test_client_with_auth("https://test.example.com");
1893        assert_eq!(client.base_url, "https://test.example.com");
1894        assert!(client.signer.is_some());
1895        assert_eq!(client.chain_id, 137);
1896    }
1897
1898    #[tokio::test(flavor = "multi_thread")]
1899    async fn test_client_with_l2_headers() {
1900        let api_creds = ApiCredentials {
1901            api_key: "test_key".to_string(),
1902            secret: "test_secret".to_string(),
1903            passphrase: "test_passphrase".to_string(),
1904        };
1905
1906        let client = ClobClient::with_l2_headers(
1907            "https://test.example.com",
1908            "0x1234567890123456789012345678901234567890123456789012345678901234",
1909            137,
1910            api_creds.clone(),
1911        );
1912
1913        assert_eq!(client.base_url, "https://test.example.com");
1914        assert!(client.signer.is_some());
1915        assert!(client.api_creds.is_some());
1916        assert_eq!(client.chain_id, 137);
1917    }
1918
1919    #[tokio::test(flavor = "multi_thread")]
1920    async fn test_set_api_creds() {
1921        let mut client = create_test_client("https://test.example.com");
1922        assert!(client.api_creds.is_none());
1923
1924        let api_creds = ApiCredentials {
1925            api_key: "test_key".to_string(),
1926            secret: "test_secret".to_string(),
1927            passphrase: "test_passphrase".to_string(),
1928        };
1929
1930        client.set_api_creds(api_creds.clone());
1931        assert!(client.api_creds.is_some());
1932        assert_eq!(client.api_creds.unwrap().api_key, "test_key");
1933    }
1934
1935    #[tokio::test(flavor = "multi_thread")]
1936    async fn test_get_sampling_markets_success() {
1937        let mut server = Server::new_async().await;
1938        let mock_response = r#"{
1939            "limit": 10,
1940            "count": 2, 
1941            "next_cursor": null,
1942            "data": [
1943                {
1944                    "condition_id": "0x123",
1945                    "tokens": [
1946                        {"token_id": "0x456", "outcome": "Yes", "price": 0.5, "winner": false},
1947                        {"token_id": "0x789", "outcome": "No", "price": 0.5, "winner": false}
1948                    ],
1949                    "rewards": {
1950                        "rates": null,
1951                        "min_size": 1.0,
1952                        "max_spread": 0.1,
1953                        "event_start_date": null,
1954                        "event_end_date": null,
1955                        "in_game_multiplier": null,
1956                        "reward_epoch": null
1957                    },
1958                    "min_incentive_size": null,
1959                    "max_incentive_spread": null,
1960                    "active": true,
1961                    "closed": false,
1962                    "question_id": "0x123",
1963                    "minimum_order_size": 1.0,
1964                    "minimum_tick_size": 0.01,
1965                    "description": "Test market",
1966                    "category": "test",
1967                    "end_date_iso": null,
1968                    "game_start_time": null,
1969                    "question": "Will this test pass?",
1970                    "market_slug": "test-market",
1971                    "seconds_delay": 0,
1972                    "icon": "",
1973                    "fpmm": ""
1974                }
1975            ]
1976        }"#;
1977
1978        let mock = server
1979            .mock("GET", "/sampling-markets")
1980            .match_query(Matcher::UrlEncoded("next_cursor".into(), "MA==".into()))
1981            .with_status(200)
1982            .with_header("content-type", "application/json")
1983            .with_body(mock_response)
1984            .create_async()
1985            .await;
1986
1987        let client = create_test_client(&server.url());
1988        let result = client.get_sampling_markets(None).await;
1989
1990        mock.assert_async().await;
1991        assert!(result.is_ok());
1992        let markets = result.unwrap();
1993        assert_eq!(markets.data.len(), 1);
1994        assert_eq!(markets.data[0].question, "Will this test pass?");
1995    }
1996
1997    #[tokio::test(flavor = "multi_thread")]
1998    async fn test_get_sampling_markets_with_cursor() {
1999        let mut server = Server::new_async().await;
2000        let mock_response = r#"{
2001            "limit": 5,
2002            "count": 0,
2003            "next_cursor": null,
2004            "data": []
2005        }"#;
2006
2007        let mock = server
2008            .mock("GET", "/sampling-markets")
2009            .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded(
2010                "next_cursor".into(),
2011                "test_cursor".into(),
2012            )]))
2013            .with_status(200)
2014            .with_header("content-type", "application/json")
2015            .with_body(mock_response)
2016            .create_async()
2017            .await;
2018
2019        let client = create_test_client(&server.url());
2020        let result = client.get_sampling_markets(Some("test_cursor")).await;
2021
2022        mock.assert_async().await;
2023        assert!(result.is_ok());
2024        let markets = result.unwrap();
2025        assert_eq!(markets.data.len(), 0);
2026    }
2027
2028    #[tokio::test(flavor = "multi_thread")]
2029    async fn test_get_order_book_success() {
2030        let mut server = Server::new_async().await;
2031        let mock_response = r#"{
2032            "market": "0x123",
2033            "asset_id": "0x123",
2034            "hash": "0xabc123",
2035            "timestamp": "1234567890",
2036            "bids": [
2037                {"price": "0.75", "size": "100.0"}
2038            ],
2039            "asks": [
2040                {"price": "0.76", "size": "50.0"}
2041            ],
2042            "min_order_size": "1",
2043            "neg_risk": false,
2044            "tick_size": "0.01",
2045            "last_trade_price": "0.755"
2046        }"#;
2047
2048        let mock = server
2049            .mock("GET", "/book")
2050            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
2051            .with_status(200)
2052            .with_header("content-type", "application/json")
2053            .with_body(mock_response)
2054            .create_async()
2055            .await;
2056
2057        let client = create_test_client(&server.url());
2058        let result = client.get_order_book("0x123").await;
2059
2060        mock.assert_async().await;
2061        assert!(result.is_ok());
2062        let book = result.unwrap();
2063        assert_eq!(book.market, "0x123");
2064        assert_eq!(book.bids.len(), 1);
2065        assert_eq!(book.asks.len(), 1);
2066        assert_eq!(book.min_order_size, Decimal::from_str("1").unwrap());
2067        assert!(!book.neg_risk);
2068        assert_eq!(book.tick_size, Decimal::from_str("0.01").unwrap());
2069        assert_eq!(
2070            book.last_trade_price,
2071            Some(Decimal::from_str("0.755").unwrap())
2072        );
2073    }
2074
2075    #[tokio::test(flavor = "multi_thread")]
2076    async fn test_get_midpoint_success() {
2077        let mut server = Server::new_async().await;
2078        let mock_response = r#"{
2079            "mid": "0.755"
2080        }"#;
2081
2082        let mock = server
2083            .mock("GET", "/midpoint")
2084            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
2085            .with_status(200)
2086            .with_header("content-type", "application/json")
2087            .with_body(mock_response)
2088            .create_async()
2089            .await;
2090
2091        let client = create_test_client(&server.url());
2092        let result = client.get_midpoint("0x123").await;
2093
2094        mock.assert_async().await;
2095        assert!(result.is_ok());
2096        let response = result.unwrap();
2097        assert_eq!(response.mid, Decimal::from_str("0.755").unwrap());
2098    }
2099
2100    #[tokio::test(flavor = "multi_thread")]
2101    async fn test_get_spread_success() {
2102        let mut server = Server::new_async().await;
2103        let mock_response = r#"{
2104            "spread": "0.01"
2105        }"#;
2106
2107        let mock = server
2108            .mock("GET", "/spread")
2109            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
2110            .with_status(200)
2111            .with_header("content-type", "application/json")
2112            .with_body(mock_response)
2113            .create_async()
2114            .await;
2115
2116        let client = create_test_client(&server.url());
2117        let result = client.get_spread("0x123").await;
2118
2119        mock.assert_async().await;
2120        assert!(result.is_ok());
2121        let response = result.unwrap();
2122        assert_eq!(response.spread, Decimal::from_str("0.01").unwrap());
2123    }
2124
2125    #[tokio::test(flavor = "multi_thread")]
2126    async fn test_get_price_success() {
2127        let mut server = Server::new_async().await;
2128        let mock_response = r#"{
2129            "price": "0.76"
2130        }"#;
2131
2132        let mock = server
2133            .mock("GET", "/price")
2134            .match_query(Matcher::AllOf(vec![
2135                Matcher::UrlEncoded("token_id".into(), "0x123".into()),
2136                Matcher::UrlEncoded("side".into(), "BUY".into()),
2137            ]))
2138            .with_status(200)
2139            .with_header("content-type", "application/json")
2140            .with_body(mock_response)
2141            .create_async()
2142            .await;
2143
2144        let client = create_test_client(&server.url());
2145        let result = client.get_price("0x123", Side::BUY).await;
2146
2147        mock.assert_async().await;
2148        assert!(result.is_ok());
2149        let response = result.unwrap();
2150        assert_eq!(response.price, Decimal::from_str("0.76").unwrap());
2151    }
2152
2153    #[tokio::test(flavor = "multi_thread")]
2154    async fn test_get_prices_history_interval_rejects_hex_condition_id() {
2155        let client = create_test_client("https://test.example.com");
2156        let result = client
2157            .get_prices_history_interval("0xdeadbeef", PricesHistoryInterval::OneDay, None)
2158            .await;
2159        assert!(matches!(result, Err(PolyfillError::Validation { .. })));
2160    }
2161
2162    #[tokio::test(flavor = "multi_thread")]
2163    async fn test_get_prices_history_interval_success() {
2164        let mut server = Server::new_async().await;
2165        let mock_response = r#"{"history":[{"t":1}]}"#;
2166
2167        let mock = server
2168            .mock("GET", "/prices-history")
2169            .match_query(Matcher::AllOf(vec![
2170                Matcher::UrlEncoded("market".into(), "12345".into()),
2171                Matcher::UrlEncoded("interval".into(), "1d".into()),
2172                Matcher::UrlEncoded("fidelity".into(), "5".into()),
2173            ]))
2174            .with_status(200)
2175            .with_header("content-type", "application/json")
2176            .with_body(mock_response)
2177            .create_async()
2178            .await;
2179
2180        let client = create_test_client(&server.url());
2181        let response = client
2182            .get_prices_history_interval("12345", PricesHistoryInterval::OneDay, Some(5))
2183            .await
2184            .unwrap();
2185
2186        mock.assert_async().await;
2187        assert_eq!(response.history.len(), 1);
2188    }
2189
2190    #[tokio::test(flavor = "multi_thread")]
2191    async fn test_get_tick_size_success() {
2192        let mut server = Server::new_async().await;
2193        let mock_response = r#"{
2194            "minimum_tick_size": "0.01"
2195        }"#;
2196
2197        let mock = server
2198            .mock("GET", "/tick-size")
2199            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
2200            .with_status(200)
2201            .with_header("content-type", "application/json")
2202            .with_body(mock_response)
2203            .create_async()
2204            .await;
2205
2206        let client = create_test_client(&server.url());
2207        let result = client.get_tick_size("0x123").await;
2208
2209        mock.assert_async().await;
2210        assert!(result.is_ok());
2211        let tick_size = result.unwrap();
2212        assert_eq!(tick_size, Decimal::from_str("0.01").unwrap());
2213    }
2214
2215    #[tokio::test(flavor = "multi_thread")]
2216    async fn test_get_neg_risk_success() {
2217        let mut server = Server::new_async().await;
2218        let mock_response = r#"{
2219            "neg_risk": false
2220        }"#;
2221
2222        let mock = server
2223            .mock("GET", "/neg-risk")
2224            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
2225            .with_status(200)
2226            .with_header("content-type", "application/json")
2227            .with_body(mock_response)
2228            .create_async()
2229            .await;
2230
2231        let client = create_test_client(&server.url());
2232        let result = client.get_neg_risk("0x123").await;
2233
2234        mock.assert_async().await;
2235        assert!(result.is_ok());
2236        let neg_risk = result.unwrap();
2237        assert!(!neg_risk);
2238    }
2239
2240    #[tokio::test(flavor = "multi_thread")]
2241    async fn test_api_error_handling() {
2242        let mut server = Server::new_async().await;
2243
2244        let mock = server
2245            .mock("GET", "/book")
2246            .match_query(Matcher::UrlEncoded(
2247                "token_id".into(),
2248                "invalid_token".into(),
2249            ))
2250            .with_status(404)
2251            .with_header("content-type", "application/json")
2252            .with_body(r#"{"error": "Market not found"}"#)
2253            .create_async()
2254            .await;
2255
2256        let client = create_test_client(&server.url());
2257        let result = client.get_order_book("invalid_token").await;
2258
2259        mock.assert_async().await;
2260        assert!(result.is_err());
2261
2262        let error = result.unwrap_err();
2263        // The error should be either Network or Api error
2264        assert!(
2265            matches!(error, PolyfillError::Network { .. })
2266                || matches!(error, PolyfillError::Api { .. })
2267        );
2268    }
2269
2270    #[tokio::test(flavor = "multi_thread")]
2271    async fn test_network_error_handling() {
2272        // Test with invalid URL to simulate network error
2273        let client = create_test_client("http://invalid-host-that-does-not-exist.com");
2274        let result = client.get_order_book("0x123").await;
2275
2276        assert!(result.is_err());
2277        let error = result.unwrap_err();
2278        assert!(matches!(error, PolyfillError::Network { .. }));
2279    }
2280
2281    #[test]
2282    fn test_client_url_validation() {
2283        let client = create_test_client("https://test.example.com");
2284        assert_eq!(client.base_url, "https://test.example.com");
2285
2286        let client2 = create_test_client("http://localhost:8080");
2287        assert_eq!(client2.base_url, "http://localhost:8080");
2288    }
2289
2290    #[tokio::test(flavor = "multi_thread")]
2291    async fn test_get_midpoints_batch() {
2292        let mut server = Server::new_async().await;
2293        let mock_response = r#"{
2294            "0x123": "0.755",
2295            "0x456": "0.623"
2296        }"#;
2297
2298        let mock = server
2299            .mock("POST", "/midpoints")
2300            .with_header("content-type", "application/json")
2301            .with_status(200)
2302            .with_header("content-type", "application/json")
2303            .with_body(mock_response)
2304            .create_async()
2305            .await;
2306
2307        let client = create_test_client(&server.url());
2308        let token_ids = vec!["0x123".to_string(), "0x456".to_string()];
2309        let result = client.get_midpoints(&token_ids).await;
2310
2311        mock.assert_async().await;
2312        assert!(result.is_ok());
2313        let midpoints = result.unwrap();
2314        assert_eq!(midpoints.len(), 2);
2315        assert_eq!(
2316            midpoints.get("0x123").unwrap(),
2317            &Decimal::from_str("0.755").unwrap()
2318        );
2319        assert_eq!(
2320            midpoints.get("0x456").unwrap(),
2321            &Decimal::from_str("0.623").unwrap()
2322        );
2323    }
2324
2325    #[test]
2326    fn test_client_configuration() {
2327        let client = create_test_client("https://test.example.com");
2328
2329        // Test initial state
2330        assert!(client.signer.is_none());
2331        assert!(client.api_creds.is_none());
2332
2333        // Test with auth
2334        let auth_client = create_test_client_with_auth("https://test.example.com");
2335        assert!(auth_client.signer.is_some());
2336        assert_eq!(auth_client.chain_id, 137);
2337    }
2338
2339    #[tokio::test(flavor = "multi_thread")]
2340    async fn test_get_ok() {
2341        let mut server = Server::new_async().await;
2342        let mock_response = r#"{"status": "ok"}"#;
2343
2344        let mock = server
2345            .mock("GET", "/ok")
2346            .with_header("content-type", "application/json")
2347            .with_status(200)
2348            .with_body(mock_response)
2349            .create_async()
2350            .await;
2351
2352        let client = create_test_client(&server.url());
2353        let result = client.get_ok().await;
2354
2355        mock.assert_async().await;
2356        assert!(result);
2357    }
2358
2359    #[tokio::test(flavor = "multi_thread")]
2360    async fn test_get_prices_batch() {
2361        let mut server = Server::new_async().await;
2362        let mock_response = r#"{
2363            "0x123": {
2364                "BUY": "0.755",
2365                "SELL": "0.745"
2366            },
2367            "0x456": {
2368                "BUY": "0.623",
2369                "SELL": "0.613"
2370            }
2371        }"#;
2372
2373        let mock = server
2374            .mock("POST", "/prices")
2375            .with_header("content-type", "application/json")
2376            .with_status(200)
2377            .with_body(mock_response)
2378            .create_async()
2379            .await;
2380
2381        let client = create_test_client(&server.url());
2382        let book_params = vec![
2383            crate::types::BookParams {
2384                token_id: "0x123".to_string(),
2385                side: Side::BUY,
2386            },
2387            crate::types::BookParams {
2388                token_id: "0x456".to_string(),
2389                side: Side::SELL,
2390            },
2391        ];
2392        let result = client.get_prices(&book_params).await;
2393
2394        mock.assert_async().await;
2395        assert!(result.is_ok());
2396        let prices = result.unwrap();
2397        assert_eq!(prices.len(), 2);
2398        assert!(prices.contains_key("0x123"));
2399        assert!(prices.contains_key("0x456"));
2400    }
2401
2402    #[tokio::test(flavor = "multi_thread")]
2403    async fn test_get_server_time() {
2404        let mut server = Server::new_async().await;
2405        let mock_response = "1234567890"; // Plain text response
2406
2407        let mock = server
2408            .mock("GET", "/time")
2409            .with_status(200)
2410            .with_body(mock_response)
2411            .create_async()
2412            .await;
2413
2414        let client = create_test_client(&server.url());
2415        let result = client.get_server_time().await;
2416
2417        mock.assert_async().await;
2418        assert!(result.is_ok());
2419        let timestamp = result.unwrap();
2420        assert_eq!(timestamp, 1234567890);
2421    }
2422
2423    #[tokio::test(flavor = "multi_thread")]
2424    async fn test_create_or_derive_api_key() {
2425        let mut server = Server::new_async().await;
2426        let mock_response = r#"{
2427            "apiKey": "test-api-key-123",
2428            "secret": "test-secret-456",
2429            "passphrase": "test-passphrase"
2430        }"#;
2431
2432        // Mock both create and derive endpoints since the method tries both
2433        let create_mock = server
2434            .mock("POST", "/auth/api-key")
2435            .with_header("content-type", "application/json")
2436            .with_status(200)
2437            .with_body(mock_response)
2438            .create_async()
2439            .await;
2440
2441        let client = create_test_client_with_auth(&server.url());
2442        let result = client.create_or_derive_api_key(None).await;
2443
2444        create_mock.assert_async().await;
2445        assert!(result.is_ok());
2446        let api_creds = result.unwrap();
2447        assert_eq!(api_creds.api_key, "test-api-key-123");
2448    }
2449
2450    #[tokio::test(flavor = "multi_thread")]
2451    async fn test_create_or_derive_api_key_falls_back_on_api_error() {
2452        let mut server = Server::new_async().await;
2453
2454        // Create fails with a status error -> should fall back to derive.
2455        let create_mock = server
2456            .mock("POST", "/auth/api-key")
2457            .with_status(400)
2458            .with_header("content-type", "application/json")
2459            .with_body(r#"{"error":"key exists"}"#)
2460            .create_async()
2461            .await;
2462
2463        let derive_mock = server
2464            .mock("GET", "/auth/derive-api-key")
2465            .with_status(200)
2466            .with_header("content-type", "application/json")
2467            .with_body(
2468                r#"{"apiKey":"derived-api-key","secret":"derived-secret","passphrase":"derived-pass"}"#,
2469            )
2470            .create_async()
2471            .await;
2472
2473        let client = create_test_client_with_auth(&server.url());
2474        let result = client.create_or_derive_api_key(None).await;
2475
2476        create_mock.assert_async().await;
2477        derive_mock.assert_async().await;
2478        assert!(result.is_ok());
2479        assert_eq!(result.unwrap().api_key, "derived-api-key");
2480    }
2481
2482    #[tokio::test(flavor = "multi_thread")]
2483    async fn test_create_or_derive_api_key_does_not_fallback_on_non_api_error() {
2484        let mut server = Server::new_async().await;
2485
2486        // Create returns 200 but with invalid JSON -> not an API status error.
2487        let create_mock = server
2488            .mock("POST", "/auth/api-key")
2489            .with_status(200)
2490            .with_header("content-type", "application/json")
2491            .with_body("not-json")
2492            .create_async()
2493            .await;
2494
2495        // If we incorrectly fall back, this would be called.
2496        let derive_mock = server
2497            .mock("GET", "/auth/derive-api-key")
2498            .with_status(200)
2499            .with_header("content-type", "application/json")
2500            .with_body(
2501                r#"{"apiKey":"derived-api-key","secret":"derived-secret","passphrase":"derived-pass"}"#,
2502            )
2503            .expect(0)
2504            .create_async()
2505            .await;
2506
2507        let client = create_test_client_with_auth(&server.url());
2508        let result = client.create_or_derive_api_key(None).await;
2509
2510        create_mock.assert_async().await;
2511        derive_mock.assert_async().await;
2512        assert!(result.is_err());
2513    }
2514    #[tokio::test(flavor = "multi_thread")]
2515    async fn test_get_order_books_batch() {
2516        let mut server = Server::new_async().await;
2517        let mock_response = r#"[
2518            {
2519                "market": "0x123",
2520                "asset_id": "0x123",
2521                "hash": "test-hash",
2522                "timestamp": "1234567890",
2523                "bids": [{"price": "0.75", "size": "100.0"}],
2524                "asks": [{"price": "0.76", "size": "50.0"}],
2525                "min_order_size": "1",
2526                "neg_risk": false,
2527                "tick_size": "0.01",
2528                "last_trade_price": null
2529            }
2530        ]"#;
2531
2532        let mock = server
2533            .mock("POST", "/books")
2534            .with_header("content-type", "application/json")
2535            .with_status(200)
2536            .with_body(mock_response)
2537            .create_async()
2538            .await;
2539
2540        let client = create_test_client(&server.url());
2541        let token_ids = vec!["0x123".to_string()];
2542        let result = client.get_order_books(&token_ids).await;
2543
2544        mock.assert_async().await;
2545        if let Err(e) = &result {
2546            println!("Error: {:?}", e);
2547        }
2548        assert!(result.is_ok());
2549        let books = result.unwrap();
2550        assert_eq!(books.len(), 1);
2551    }
2552
2553    #[tokio::test(flavor = "multi_thread")]
2554    async fn test_order_args_creation() {
2555        // Test OrderArgs creation and default values
2556        let order_args = ClientOrderArgs::new(
2557            "0x123",
2558            Decimal::from_str("0.75").unwrap(),
2559            Decimal::from_str("100.0").unwrap(),
2560            Side::BUY,
2561        );
2562
2563        assert_eq!(order_args.token_id, "0x123");
2564        assert_eq!(order_args.price, Decimal::from_str("0.75").unwrap());
2565        assert_eq!(order_args.size, Decimal::from_str("100.0").unwrap());
2566        assert_eq!(order_args.side, Side::BUY);
2567
2568        // Test default
2569        let default_args = ClientOrderArgs::default();
2570        assert_eq!(default_args.token_id, "");
2571        assert_eq!(default_args.price, Decimal::ZERO);
2572        assert_eq!(default_args.size, Decimal::ZERO);
2573        assert_eq!(default_args.side, Side::BUY);
2574    }
2575}