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