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        // Owner field must reference the credential principal identifier
839        // to maintain consistency with the authentication context layer
840        let body = PostOrder::new(order, api_creds.api_key.clone(), order_type);
841
842        let headers = create_l2_headers(signer, api_creds, "POST", "/order", Some(&body))?;
843        let req = self.create_request_with_headers(Method::POST, "/order", headers.into_iter());
844
845        let response = req.json(&body).send().await?;
846        if !response.status().is_success() {
847            return Err(PolyfillError::api(
848                response.status().as_u16(),
849                "Failed to post order",
850            ));
851        }
852
853        Ok(response.json::<Value>().await?)
854    }
855
856    /// Create and post an order in one call
857    pub async fn create_and_post_order(&self, order_args: &OrderArgs) -> Result<Value> {
858        let order = self.create_order(order_args, None, None, None).await?;
859        self.post_order(order, OrderType::GTC).await
860    }
861
862    /// Cancel an order
863    pub async fn cancel(&self, order_id: &str) -> Result<Value> {
864        let signer = self
865            .signer
866            .as_ref()
867            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
868        let api_creds = self
869            .api_creds
870            .as_ref()
871            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
872
873        let body = std::collections::HashMap::from([("orderID", order_id)]);
874
875        let headers = create_l2_headers(signer, api_creds, "DELETE", "/order", Some(&body))?;
876        let req = self.create_request_with_headers(Method::DELETE, "/order", headers.into_iter());
877
878        let response = req.json(&body).send().await?;
879        if !response.status().is_success() {
880            return Err(PolyfillError::api(
881                response.status().as_u16(),
882                "Failed to cancel order",
883            ));
884        }
885
886        Ok(response.json::<Value>().await?)
887    }
888
889    /// Cancel multiple orders
890    pub async fn cancel_orders(&self, order_ids: &[String]) -> Result<Value> {
891        let signer = self
892            .signer
893            .as_ref()
894            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
895        let api_creds = self
896            .api_creds
897            .as_ref()
898            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
899
900        let headers = create_l2_headers(signer, api_creds, "DELETE", "/orders", Some(order_ids))?;
901        let req = self.create_request_with_headers(Method::DELETE, "/orders", headers.into_iter());
902
903        let response = req.json(order_ids).send().await?;
904        if !response.status().is_success() {
905            return Err(PolyfillError::api(
906                response.status().as_u16(),
907                "Failed to cancel orders",
908            ));
909        }
910
911        Ok(response.json::<Value>().await?)
912    }
913
914    /// Cancel all orders
915    pub async fn cancel_all(&self) -> Result<Value> {
916        let signer = self
917            .signer
918            .as_ref()
919            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
920        let api_creds = self
921            .api_creds
922            .as_ref()
923            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
924
925        let headers = create_l2_headers::<Value>(signer, api_creds, "DELETE", "/cancel-all", None)?;
926        let req =
927            self.create_request_with_headers(Method::DELETE, "/cancel-all", headers.into_iter());
928
929        let response = req.send().await?;
930        if !response.status().is_success() {
931            return Err(PolyfillError::api(
932                response.status().as_u16(),
933                "Failed to cancel all orders",
934            ));
935        }
936
937        Ok(response.json::<Value>().await?)
938    }
939
940    /// Get open orders with optional filtering
941    ///
942    /// This retrieves all open orders for the authenticated user. You can filter by:
943    /// - Order ID (exact match)
944    /// - Asset/Token ID (all orders for a specific token)
945    /// - Market ID (all orders for a specific market)
946    ///
947    /// The response includes order status, fill information, and timestamps.
948    pub async fn get_orders(
949        &self,
950        params: Option<&crate::types::OpenOrderParams>,
951        next_cursor: Option<&str>,
952    ) -> Result<Vec<crate::types::OpenOrder>> {
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        let method = Method::GET;
963        let endpoint = "/data/orders";
964        let headers =
965            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
966
967        let query_params = match params {
968            None => Vec::new(),
969            Some(p) => p.to_query_params(),
970        };
971
972        let mut next_cursor = next_cursor.unwrap_or("MA==").to_string(); // INITIAL_CURSOR
973        let mut output = Vec::new();
974
975        while next_cursor != "LTE=" {
976            // END_CURSOR
977            let req = self
978                .http_client
979                .request(method.clone(), format!("{}{}", self.base_url, endpoint))
980                .query(&query_params)
981                .query(&[("next_cursor", &next_cursor)]);
982
983            let r = headers
984                .clone()
985                .into_iter()
986                .fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v));
987
988            let resp = r
989                .send()
990                .await
991                .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?
992                .json::<Value>()
993                .await
994                .map_err(|e| {
995                    PolyfillError::parse(format!("Failed to parse response: {}", e), None)
996                })?;
997
998            let new_cursor = resp["next_cursor"]
999                .as_str()
1000                .ok_or_else(|| {
1001                    PolyfillError::parse("Failed to parse next cursor".to_string(), None)
1002                })?
1003                .to_owned();
1004
1005            next_cursor = new_cursor;
1006
1007            let results = resp["data"].clone();
1008            let orders =
1009                serde_json::from_value::<Vec<crate::types::OpenOrder>>(results).map_err(|e| {
1010                    PolyfillError::parse(
1011                        format!("Failed to parse data from order response: {}", e),
1012                        None,
1013                    )
1014                })?;
1015            output.extend(orders);
1016        }
1017
1018        Ok(output)
1019    }
1020
1021    /// Get trade history with optional filtering
1022    ///
1023    /// This retrieves historical trades for the authenticated user. You can filter by:
1024    /// - Trade ID (exact match)
1025    /// - Maker address (trades where you were the maker)
1026    /// - Market ID (trades in a specific market)
1027    /// - Asset/Token ID (trades for a specific token)
1028    /// - Time range (before/after timestamps)
1029    ///
1030    /// Trades are returned in reverse chronological order (newest first).
1031    pub async fn get_trades(
1032        &self,
1033        trade_params: Option<&crate::types::TradeParams>,
1034        next_cursor: Option<&str>,
1035    ) -> Result<Vec<Value>> {
1036        let signer = self
1037            .signer
1038            .as_ref()
1039            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1040        let api_creds = self
1041            .api_creds
1042            .as_ref()
1043            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1044
1045        let method = Method::GET;
1046        let endpoint = "/data/trades";
1047        let headers =
1048            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1049
1050        let query_params = match trade_params {
1051            None => Vec::new(),
1052            Some(p) => p.to_query_params(),
1053        };
1054
1055        let mut next_cursor = next_cursor.unwrap_or("MA==").to_string(); // INITIAL_CURSOR
1056        let mut output = Vec::new();
1057
1058        while next_cursor != "LTE=" {
1059            // END_CURSOR
1060            let req = self
1061                .http_client
1062                .request(method.clone(), format!("{}{}", self.base_url, endpoint))
1063                .query(&query_params)
1064                .query(&[("next_cursor", &next_cursor)]);
1065
1066            let r = headers
1067                .clone()
1068                .into_iter()
1069                .fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v));
1070
1071            let resp = r
1072                .send()
1073                .await
1074                .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?
1075                .json::<Value>()
1076                .await
1077                .map_err(|e| {
1078                    PolyfillError::parse(format!("Failed to parse response: {}", e), None)
1079                })?;
1080
1081            let new_cursor = resp["next_cursor"]
1082                .as_str()
1083                .ok_or_else(|| {
1084                    PolyfillError::parse("Failed to parse next cursor".to_string(), None)
1085                })?
1086                .to_owned();
1087
1088            next_cursor = new_cursor;
1089
1090            let results = resp["data"].clone();
1091            output.push(results);
1092        }
1093
1094        Ok(output)
1095    }
1096
1097    /// Get balance and allowance information for all assets
1098    ///
1099    /// This returns the current balance and allowance for each asset in your account.
1100    /// Balance is how much you own, allowance is how much the exchange can spend on your behalf.
1101    ///
1102    /// You need both balance and allowance to place orders - the exchange needs permission
1103    /// to move your tokens when orders are filled.
1104    pub async fn get_balance_allowance(
1105        &self,
1106        params: Option<crate::types::BalanceAllowanceParams>,
1107    ) -> Result<Value> {
1108        let signer = self
1109            .signer
1110            .as_ref()
1111            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1112        let api_creds = self
1113            .api_creds
1114            .as_ref()
1115            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1116
1117        let mut params = params.unwrap_or_default();
1118        if params.signature_type.is_none() {
1119            params.set_signature_type(
1120                self.order_builder
1121                    .as_ref()
1122                    .expect("OrderBuilder not set")
1123                    .get_sig_type(),
1124            );
1125        }
1126
1127        let query_params = params.to_query_params();
1128
1129        let method = Method::GET;
1130        let endpoint = "/balance-allowance";
1131        let headers =
1132            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1133
1134        let response = self
1135            .http_client
1136            .request(method, format!("{}{}", self.base_url, endpoint))
1137            .headers(
1138                headers
1139                    .into_iter()
1140                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1141                    .collect(),
1142            )
1143            .query(&query_params)
1144            .send()
1145            .await
1146            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1147
1148        response
1149            .json::<Value>()
1150            .await
1151            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1152    }
1153
1154    /// Set up notifications for order fills and other events
1155    ///
1156    /// This configures push notifications so you get alerted when:
1157    /// - Your orders get filled
1158    /// - Your orders get cancelled
1159    /// - Market conditions change significantly
1160    ///
1161    /// The signature proves you own the account and want to receive notifications.
1162    pub async fn get_notifications(&self) -> Result<Value> {
1163        let signer = self
1164            .signer
1165            .as_ref()
1166            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1167        let api_creds = self
1168            .api_creds
1169            .as_ref()
1170            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1171
1172        let method = Method::GET;
1173        let endpoint = "/notifications";
1174        let headers =
1175            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1176
1177        let response = self
1178            .http_client
1179            .request(method, format!("{}{}", self.base_url, endpoint))
1180            .headers(
1181                headers
1182                    .into_iter()
1183                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1184                    .collect(),
1185            )
1186            .query(&[(
1187                "signature_type",
1188                &self
1189                    .order_builder
1190                    .as_ref()
1191                    .expect("OrderBuilder not set")
1192                    .get_sig_type()
1193                    .to_string(),
1194            )])
1195            .send()
1196            .await
1197            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1198
1199        response
1200            .json::<Value>()
1201            .await
1202            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1203    }
1204
1205    /// Get midpoints for multiple tokens in a single request
1206    ///
1207    /// This is much more efficient than calling get_midpoint() multiple times.
1208    /// Instead of N round trips, you make just 1 request and get all the midpoints back.
1209    ///
1210    /// Midpoints are returned as a HashMap where the key is the token_id and the value
1211    /// is the midpoint price (or None if there's no valid midpoint).
1212    pub async fn get_midpoints(
1213        &self,
1214        token_ids: &[String],
1215    ) -> Result<std::collections::HashMap<String, Decimal>> {
1216        let request_data: Vec<std::collections::HashMap<&str, String>> = token_ids
1217            .iter()
1218            .map(|id| {
1219                let mut map = std::collections::HashMap::new();
1220                map.insert("token_id", id.clone());
1221                map
1222            })
1223            .collect();
1224
1225        let response = self
1226            .http_client
1227            .post(format!("{}/midpoints", self.base_url))
1228            .json(&request_data)
1229            .send()
1230            .await?;
1231
1232        if !response.status().is_success() {
1233            return Err(PolyfillError::api(
1234                response.status().as_u16(),
1235                "Failed to get batch midpoints",
1236            ));
1237        }
1238
1239        let midpoints: std::collections::HashMap<String, Decimal> = response.json().await?;
1240        Ok(midpoints)
1241    }
1242
1243    /// Get bid/ask/mid prices for multiple tokens in a single request
1244    ///
1245    /// This gives you the full price picture for multiple tokens at once.
1246    /// Much more efficient than individual calls, especially when you're tracking
1247    /// a portfolio or comparing multiple markets.
1248    ///
1249    /// Returns bid (best buy price), ask (best sell price), and mid (average) for each token.
1250    pub async fn get_prices(
1251        &self,
1252        book_params: &[crate::types::BookParams],
1253    ) -> Result<std::collections::HashMap<String, std::collections::HashMap<Side, Decimal>>> {
1254        let request_data: Vec<std::collections::HashMap<&str, String>> = book_params
1255            .iter()
1256            .map(|params| {
1257                let mut map = std::collections::HashMap::new();
1258                map.insert("token_id", params.token_id.clone());
1259                map.insert("side", params.side.as_str().to_string());
1260                map
1261            })
1262            .collect();
1263
1264        let response = self
1265            .http_client
1266            .post(format!("{}/prices", self.base_url))
1267            .json(&request_data)
1268            .send()
1269            .await?;
1270
1271        if !response.status().is_success() {
1272            return Err(PolyfillError::api(
1273                response.status().as_u16(),
1274                "Failed to get batch prices",
1275            ));
1276        }
1277
1278        let prices: std::collections::HashMap<String, std::collections::HashMap<Side, Decimal>> =
1279            response.json().await?;
1280        Ok(prices)
1281    }
1282
1283    /// Get order book for multiple tokens (batch) - reference implementation compatible
1284    pub async fn get_order_books(&self, token_ids: &[String]) -> Result<Vec<OrderBookSummary>> {
1285        let request_data: Vec<std::collections::HashMap<&str, String>> = token_ids
1286            .iter()
1287            .map(|id| {
1288                let mut map = std::collections::HashMap::new();
1289                map.insert("token_id", id.clone());
1290                map
1291            })
1292            .collect();
1293
1294        let response = self
1295            .http_client
1296            .post(format!("{}/books", self.base_url))
1297            .json(&request_data)
1298            .send()
1299            .await
1300            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1301
1302        response
1303            .json::<Vec<OrderBookSummary>>()
1304            .await
1305            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1306    }
1307
1308    /// Get single order by ID
1309    pub async fn get_order(&self, order_id: &str) -> Result<crate::types::OpenOrder> {
1310        let signer = self
1311            .signer
1312            .as_ref()
1313            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1314        let api_creds = self
1315            .api_creds
1316            .as_ref()
1317            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1318
1319        let method = Method::GET;
1320        let endpoint = &format!("/data/order/{}", order_id);
1321        let headers =
1322            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1323
1324        let response = self
1325            .http_client
1326            .request(method, format!("{}{}", self.base_url, endpoint))
1327            .headers(
1328                headers
1329                    .into_iter()
1330                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1331                    .collect(),
1332            )
1333            .send()
1334            .await
1335            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1336
1337        response
1338            .json::<crate::types::OpenOrder>()
1339            .await
1340            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1341    }
1342
1343    /// Get last trade price for a token
1344    pub async fn get_last_trade_price(&self, token_id: &str) -> Result<Value> {
1345        let response = self
1346            .http_client
1347            .get(format!("{}/last-trade-price", self.base_url))
1348            .query(&[("token_id", token_id)])
1349            .send()
1350            .await
1351            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1352
1353        response
1354            .json::<Value>()
1355            .await
1356            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1357    }
1358
1359    /// Get last trade prices for multiple tokens
1360    pub async fn get_last_trade_prices(&self, token_ids: &[String]) -> Result<Value> {
1361        let request_data: Vec<std::collections::HashMap<&str, String>> = token_ids
1362            .iter()
1363            .map(|id| {
1364                let mut map = std::collections::HashMap::new();
1365                map.insert("token_id", id.clone());
1366                map
1367            })
1368            .collect();
1369
1370        let response = self
1371            .http_client
1372            .post(format!("{}/last-trades-prices", self.base_url))
1373            .json(&request_data)
1374            .send()
1375            .await
1376            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1377
1378        response
1379            .json::<Value>()
1380            .await
1381            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1382    }
1383
1384    /// Cancel market orders with optional filters
1385    pub async fn cancel_market_orders(
1386        &self,
1387        market: Option<&str>,
1388        asset_id: Option<&str>,
1389    ) -> Result<Value> {
1390        let signer = self
1391            .signer
1392            .as_ref()
1393            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1394        let api_creds = self
1395            .api_creds
1396            .as_ref()
1397            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1398
1399        let method = Method::DELETE;
1400        let endpoint = "/cancel-market-orders";
1401        let body = std::collections::HashMap::from([
1402            ("market", market.unwrap_or("")),
1403            ("asset_id", asset_id.unwrap_or("")),
1404        ]);
1405
1406        let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(&body))?;
1407
1408        let response = self
1409            .http_client
1410            .request(method, format!("{}{}", self.base_url, endpoint))
1411            .headers(
1412                headers
1413                    .into_iter()
1414                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1415                    .collect(),
1416            )
1417            .json(&body)
1418            .send()
1419            .await
1420            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1421
1422        response
1423            .json::<Value>()
1424            .await
1425            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1426    }
1427
1428    /// Drop (delete) notifications by IDs
1429    pub async fn drop_notifications(&self, ids: &[String]) -> Result<Value> {
1430        let signer = self
1431            .signer
1432            .as_ref()
1433            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1434        let api_creds = self
1435            .api_creds
1436            .as_ref()
1437            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1438
1439        let method = Method::DELETE;
1440        let endpoint = "/notifications";
1441        let headers =
1442            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1443
1444        let response = self
1445            .http_client
1446            .request(method, format!("{}{}", self.base_url, endpoint))
1447            .headers(
1448                headers
1449                    .into_iter()
1450                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1451                    .collect(),
1452            )
1453            .query(&[("ids", ids.join(","))])
1454            .send()
1455            .await
1456            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1457
1458        response
1459            .json::<Value>()
1460            .await
1461            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1462    }
1463
1464    /// Update balance allowance
1465    pub async fn update_balance_allowance(
1466        &self,
1467        params: Option<crate::types::BalanceAllowanceParams>,
1468    ) -> Result<Value> {
1469        let signer = self
1470            .signer
1471            .as_ref()
1472            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1473        let api_creds = self
1474            .api_creds
1475            .as_ref()
1476            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1477
1478        let mut params = params.unwrap_or_default();
1479        if params.signature_type.is_none() {
1480            params.set_signature_type(
1481                self.order_builder
1482                    .as_ref()
1483                    .expect("OrderBuilder not set")
1484                    .get_sig_type(),
1485            );
1486        }
1487
1488        let query_params = params.to_query_params();
1489
1490        let method = Method::GET;
1491        let endpoint = "/balance-allowance/update";
1492        let headers =
1493            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1494
1495        let response = self
1496            .http_client
1497            .request(method, format!("{}{}", self.base_url, endpoint))
1498            .headers(
1499                headers
1500                    .into_iter()
1501                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1502                    .collect(),
1503            )
1504            .query(&query_params)
1505            .send()
1506            .await
1507            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1508
1509        response
1510            .json::<Value>()
1511            .await
1512            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1513    }
1514
1515    /// Check if an order is scoring
1516    pub async fn is_order_scoring(&self, order_id: &str) -> Result<bool> {
1517        let signer = self
1518            .signer
1519            .as_ref()
1520            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1521        let api_creds = self
1522            .api_creds
1523            .as_ref()
1524            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1525
1526        let method = Method::GET;
1527        let endpoint = "/order-scoring";
1528        let headers =
1529            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1530
1531        let response = self
1532            .http_client
1533            .request(method, format!("{}{}", self.base_url, endpoint))
1534            .headers(
1535                headers
1536                    .into_iter()
1537                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1538                    .collect(),
1539            )
1540            .query(&[("order_id", order_id)])
1541            .send()
1542            .await
1543            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1544
1545        let result: Value = response
1546            .json()
1547            .await
1548            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?;
1549
1550        Ok(result["scoring"].as_bool().unwrap_or(false))
1551    }
1552
1553    /// Check if multiple orders are scoring
1554    pub async fn are_orders_scoring(
1555        &self,
1556        order_ids: &[&str],
1557    ) -> Result<std::collections::HashMap<String, bool>> {
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::POST;
1568        let endpoint = "/orders-scoring";
1569        let headers = create_l2_headers(
1570            signer,
1571            api_creds,
1572            method.as_str(),
1573            endpoint,
1574            Some(order_ids),
1575        )?;
1576
1577        let response = self
1578            .http_client
1579            .request(method, format!("{}{}", self.base_url, endpoint))
1580            .headers(
1581                headers
1582                    .into_iter()
1583                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1584                    .collect(),
1585            )
1586            .json(order_ids)
1587            .send()
1588            .await
1589            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1590
1591        response
1592            .json::<std::collections::HashMap<String, bool>>()
1593            .await
1594            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1595    }
1596
1597    /// Get sampling markets with pagination
1598    pub async fn get_sampling_markets(
1599        &self,
1600        next_cursor: Option<&str>,
1601    ) -> Result<crate::types::MarketsResponse> {
1602        let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR
1603
1604        let response = self
1605            .http_client
1606            .get(format!("{}/sampling-markets", self.base_url))
1607            .query(&[("next_cursor", next_cursor)])
1608            .send()
1609            .await
1610            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1611
1612        response
1613            .json::<crate::types::MarketsResponse>()
1614            .await
1615            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1616    }
1617
1618    /// Get sampling simplified markets with pagination
1619    pub async fn get_sampling_simplified_markets(
1620        &self,
1621        next_cursor: Option<&str>,
1622    ) -> Result<crate::types::SimplifiedMarketsResponse> {
1623        let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR
1624
1625        let response = self
1626            .http_client
1627            .get(format!("{}/sampling-simplified-markets", self.base_url))
1628            .query(&[("next_cursor", next_cursor)])
1629            .send()
1630            .await
1631            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1632
1633        response
1634            .json::<crate::types::SimplifiedMarketsResponse>()
1635            .await
1636            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1637    }
1638
1639    /// Get markets with pagination
1640    pub async fn get_markets(
1641        &self,
1642        next_cursor: Option<&str>,
1643    ) -> Result<crate::types::MarketsResponse> {
1644        let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR
1645
1646        let response = self
1647            .http_client
1648            .get(format!("{}/markets", self.base_url))
1649            .query(&[("next_cursor", next_cursor)])
1650            .send()
1651            .await
1652            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1653
1654        response
1655            .json::<crate::types::MarketsResponse>()
1656            .await
1657            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1658    }
1659
1660    /// Get simplified markets with pagination
1661    pub async fn get_simplified_markets(
1662        &self,
1663        next_cursor: Option<&str>,
1664    ) -> Result<crate::types::SimplifiedMarketsResponse> {
1665        let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR
1666
1667        let response = self
1668            .http_client
1669            .get(format!("{}/simplified-markets", self.base_url))
1670            .query(&[("next_cursor", next_cursor)])
1671            .send()
1672            .await
1673            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1674
1675        response
1676            .json::<crate::types::SimplifiedMarketsResponse>()
1677            .await
1678            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1679    }
1680
1681    /// Get single market by condition ID
1682    pub async fn get_market(&self, condition_id: &str) -> Result<crate::types::Market> {
1683        let response = self
1684            .http_client
1685            .get(format!("{}/markets/{}", self.base_url, condition_id))
1686            .send()
1687            .await
1688            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1689
1690        response
1691            .json::<crate::types::Market>()
1692            .await
1693            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1694    }
1695
1696    /// Get market trades events
1697    pub async fn get_market_trades_events(&self, condition_id: &str) -> Result<Value> {
1698        let response = self
1699            .http_client
1700            .get(format!(
1701                "{}/live-activity/events/{}",
1702                self.base_url, condition_id
1703            ))
1704            .send()
1705            .await
1706            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1707
1708        response
1709            .json::<Value>()
1710            .await
1711            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1712    }
1713}
1714
1715// Re-export types from the canonical location in types.rs
1716pub use crate::types::{
1717    ExtraOrderArgs, Market, MarketOrderArgs, MarketsResponse, MidpointResponse, NegRiskResponse,
1718    OrderBookSummary, OrderSummary, PriceResponse, Rewards, SpreadResponse, TickSizeResponse,
1719    Token,
1720};
1721
1722// Compatibility types that need to stay in client.rs
1723#[derive(Debug, Default)]
1724pub struct CreateOrderOptions {
1725    pub tick_size: Option<Decimal>,
1726    pub neg_risk: Option<bool>,
1727}
1728
1729// Re-export for compatibility
1730pub type PolyfillClient = ClobClient;
1731
1732#[cfg(test)]
1733mod tests {
1734    use super::{ClobClient, OrderArgs as ClientOrderArgs};
1735    use crate::types::Side;
1736    use crate::{ApiCredentials, PolyfillError};
1737    use mockito::{Matcher, Server};
1738    use rust_decimal::Decimal;
1739    use std::str::FromStr;
1740    use tokio;
1741
1742    fn create_test_client(base_url: &str) -> ClobClient {
1743        ClobClient::new(base_url)
1744    }
1745
1746    fn create_test_client_with_auth(base_url: &str) -> ClobClient {
1747        ClobClient::with_l1_headers(
1748            base_url,
1749            "0x1234567890123456789012345678901234567890123456789012345678901234",
1750            137,
1751        )
1752    }
1753
1754    #[tokio::test]
1755    async fn test_client_creation() {
1756        let client = create_test_client("https://test.example.com");
1757        assert_eq!(client.base_url, "https://test.example.com");
1758        assert!(client.signer.is_none());
1759        assert!(client.api_creds.is_none());
1760    }
1761
1762    #[tokio::test]
1763    async fn test_client_with_l1_headers() {
1764        let client = create_test_client_with_auth("https://test.example.com");
1765        assert_eq!(client.base_url, "https://test.example.com");
1766        assert!(client.signer.is_some());
1767        assert_eq!(client.chain_id, 137);
1768    }
1769
1770    #[tokio::test]
1771    async fn test_client_with_l2_headers() {
1772        let api_creds = ApiCredentials {
1773            api_key: "test_key".to_string(),
1774            secret: "test_secret".to_string(),
1775            passphrase: "test_passphrase".to_string(),
1776        };
1777
1778        let client = ClobClient::with_l2_headers(
1779            "https://test.example.com",
1780            "0x1234567890123456789012345678901234567890123456789012345678901234",
1781            137,
1782            api_creds.clone(),
1783        );
1784
1785        assert_eq!(client.base_url, "https://test.example.com");
1786        assert!(client.signer.is_some());
1787        assert!(client.api_creds.is_some());
1788        assert_eq!(client.chain_id, 137);
1789    }
1790
1791    #[tokio::test]
1792    async fn test_set_api_creds() {
1793        let mut client = create_test_client("https://test.example.com");
1794        assert!(client.api_creds.is_none());
1795
1796        let api_creds = ApiCredentials {
1797            api_key: "test_key".to_string(),
1798            secret: "test_secret".to_string(),
1799            passphrase: "test_passphrase".to_string(),
1800        };
1801
1802        client.set_api_creds(api_creds.clone());
1803        assert!(client.api_creds.is_some());
1804        assert_eq!(client.api_creds.unwrap().api_key, "test_key");
1805    }
1806
1807    #[tokio::test]
1808    async fn test_get_sampling_markets_success() {
1809        let mut server = Server::new_async().await;
1810        let mock_response = r#"{
1811            "limit": "10",
1812            "count": "2", 
1813            "next_cursor": null,
1814            "data": [
1815                {
1816                    "condition_id": "0x123",
1817                    "tokens": [
1818                        {"token_id": "0x456", "outcome": "Yes"},
1819                        {"token_id": "0x789", "outcome": "No"}
1820                    ],
1821                    "rewards": {
1822                        "rates": null,
1823                        "min_size": "1.0",
1824                        "max_spread": "0.1",
1825                        "event_start_date": null,
1826                        "event_end_date": null,
1827                        "in_game_multiplier": null,
1828                        "reward_epoch": null
1829                    },
1830                    "min_incentive_size": null,
1831                    "max_incentive_spread": null,
1832                    "active": true,
1833                    "closed": false,
1834                    "question_id": "0x123",
1835                    "minimum_order_size": "1.0",
1836                    "minimum_tick_size": "0.01",
1837                    "description": "Test market",
1838                    "category": "test",
1839                    "end_date_iso": null,
1840                    "game_start_time": null,
1841                    "question": "Will this test pass?",
1842                    "market_slug": "test-market",
1843                    "seconds_delay": "0",
1844                    "icon": "",
1845                    "fpmm": ""
1846                }
1847            ]
1848        }"#;
1849
1850        let mock = server
1851            .mock("GET", "/sampling-markets")
1852            .match_query(Matcher::UrlEncoded("next_cursor".into(), "MA==".into()))
1853            .with_status(200)
1854            .with_header("content-type", "application/json")
1855            .with_body(mock_response)
1856            .create_async()
1857            .await;
1858
1859        let client = create_test_client(&server.url());
1860        let result = client.get_sampling_markets(None).await;
1861
1862        mock.assert_async().await;
1863        assert!(result.is_ok());
1864        let markets = result.unwrap();
1865        assert_eq!(markets.data.len(), 1);
1866        assert_eq!(markets.data[0].question, "Will this test pass?");
1867    }
1868
1869    #[tokio::test]
1870    async fn test_get_sampling_markets_with_cursor() {
1871        let mut server = Server::new_async().await;
1872        let mock_response = r#"{
1873            "limit": "5",
1874            "count": "0",
1875            "next_cursor": null,
1876            "data": []
1877        }"#;
1878
1879        let mock = server
1880            .mock("GET", "/sampling-markets")
1881            .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded(
1882                "next_cursor".into(),
1883                "test_cursor".into(),
1884            )]))
1885            .with_status(200)
1886            .with_header("content-type", "application/json")
1887            .with_body(mock_response)
1888            .create_async()
1889            .await;
1890
1891        let client = create_test_client(&server.url());
1892        let result = client.get_sampling_markets(Some("test_cursor")).await;
1893
1894        mock.assert_async().await;
1895        assert!(result.is_ok());
1896        let markets = result.unwrap();
1897        assert_eq!(markets.data.len(), 0);
1898    }
1899
1900    #[tokio::test]
1901    async fn test_get_order_book_success() {
1902        let mut server = Server::new_async().await;
1903        let mock_response = r#"{
1904            "market": "0x123",
1905            "asset_id": "0x123",
1906            "hash": "0xabc123",
1907            "timestamp": "1234567890",
1908            "bids": [
1909                {"price": "0.75", "size": "100.0"}
1910            ],
1911            "asks": [
1912                {"price": "0.76", "size": "50.0"}
1913            ]
1914        }"#;
1915
1916        let mock = server
1917            .mock("GET", "/book")
1918            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
1919            .with_status(200)
1920            .with_header("content-type", "application/json")
1921            .with_body(mock_response)
1922            .create_async()
1923            .await;
1924
1925        let client = create_test_client(&server.url());
1926        let result = client.get_order_book("0x123").await;
1927
1928        mock.assert_async().await;
1929        assert!(result.is_ok());
1930        let book = result.unwrap();
1931        assert_eq!(book.market, "0x123");
1932        assert_eq!(book.bids.len(), 1);
1933        assert_eq!(book.asks.len(), 1);
1934    }
1935
1936    #[tokio::test]
1937    async fn test_get_midpoint_success() {
1938        let mut server = Server::new_async().await;
1939        let mock_response = r#"{
1940            "mid": "0.755"
1941        }"#;
1942
1943        let mock = server
1944            .mock("GET", "/midpoint")
1945            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
1946            .with_status(200)
1947            .with_header("content-type", "application/json")
1948            .with_body(mock_response)
1949            .create_async()
1950            .await;
1951
1952        let client = create_test_client(&server.url());
1953        let result = client.get_midpoint("0x123").await;
1954
1955        mock.assert_async().await;
1956        assert!(result.is_ok());
1957        let response = result.unwrap();
1958        assert_eq!(response.mid, Decimal::from_str("0.755").unwrap());
1959    }
1960
1961    #[tokio::test]
1962    async fn test_get_spread_success() {
1963        let mut server = Server::new_async().await;
1964        let mock_response = r#"{
1965            "spread": "0.01"
1966        }"#;
1967
1968        let mock = server
1969            .mock("GET", "/spread")
1970            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
1971            .with_status(200)
1972            .with_header("content-type", "application/json")
1973            .with_body(mock_response)
1974            .create_async()
1975            .await;
1976
1977        let client = create_test_client(&server.url());
1978        let result = client.get_spread("0x123").await;
1979
1980        mock.assert_async().await;
1981        assert!(result.is_ok());
1982        let response = result.unwrap();
1983        assert_eq!(response.spread, Decimal::from_str("0.01").unwrap());
1984    }
1985
1986    #[tokio::test]
1987    async fn test_get_price_success() {
1988        let mut server = Server::new_async().await;
1989        let mock_response = r#"{
1990            "price": "0.76"
1991        }"#;
1992
1993        let mock = server
1994            .mock("GET", "/price")
1995            .match_query(Matcher::AllOf(vec![
1996                Matcher::UrlEncoded("token_id".into(), "0x123".into()),
1997                Matcher::UrlEncoded("side".into(), "BUY".into()),
1998            ]))
1999            .with_status(200)
2000            .with_header("content-type", "application/json")
2001            .with_body(mock_response)
2002            .create_async()
2003            .await;
2004
2005        let client = create_test_client(&server.url());
2006        let result = client.get_price("0x123", Side::BUY).await;
2007
2008        mock.assert_async().await;
2009        assert!(result.is_ok());
2010        let response = result.unwrap();
2011        assert_eq!(response.price, Decimal::from_str("0.76").unwrap());
2012    }
2013
2014    #[tokio::test]
2015    async fn test_get_tick_size_success() {
2016        let mut server = Server::new_async().await;
2017        let mock_response = r#"{
2018            "minimum_tick_size": "0.01"
2019        }"#;
2020
2021        let mock = server
2022            .mock("GET", "/tick-size")
2023            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
2024            .with_status(200)
2025            .with_header("content-type", "application/json")
2026            .with_body(mock_response)
2027            .create_async()
2028            .await;
2029
2030        let client = create_test_client(&server.url());
2031        let result = client.get_tick_size("0x123").await;
2032
2033        mock.assert_async().await;
2034        assert!(result.is_ok());
2035        let tick_size = result.unwrap();
2036        assert_eq!(tick_size, Decimal::from_str("0.01").unwrap());
2037    }
2038
2039    #[tokio::test]
2040    async fn test_get_neg_risk_success() {
2041        let mut server = Server::new_async().await;
2042        let mock_response = r#"{
2043            "neg_risk": false
2044        }"#;
2045
2046        let mock = server
2047            .mock("GET", "/neg-risk")
2048            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
2049            .with_status(200)
2050            .with_header("content-type", "application/json")
2051            .with_body(mock_response)
2052            .create_async()
2053            .await;
2054
2055        let client = create_test_client(&server.url());
2056        let result = client.get_neg_risk("0x123").await;
2057
2058        mock.assert_async().await;
2059        assert!(result.is_ok());
2060        let neg_risk = result.unwrap();
2061        assert!(!neg_risk);
2062    }
2063
2064    #[tokio::test]
2065    async fn test_api_error_handling() {
2066        let mut server = Server::new_async().await;
2067
2068        let mock = server
2069            .mock("GET", "/book")
2070            .match_query(Matcher::UrlEncoded(
2071                "token_id".into(),
2072                "invalid_token".into(),
2073            ))
2074            .with_status(404)
2075            .with_header("content-type", "application/json")
2076            .with_body(r#"{"error": "Market not found"}"#)
2077            .create_async()
2078            .await;
2079
2080        let client = create_test_client(&server.url());
2081        let result = client.get_order_book("invalid_token").await;
2082
2083        mock.assert_async().await;
2084        assert!(result.is_err());
2085
2086        let error = result.unwrap_err();
2087        // The error should be either Network or Api error
2088        assert!(
2089            matches!(error, PolyfillError::Network { .. })
2090                || matches!(error, PolyfillError::Api { .. })
2091        );
2092    }
2093
2094    #[tokio::test]
2095    async fn test_network_error_handling() {
2096        // Test with invalid URL to simulate network error
2097        let client = create_test_client("http://invalid-host-that-does-not-exist.com");
2098        let result = client.get_order_book("0x123").await;
2099
2100        assert!(result.is_err());
2101        let error = result.unwrap_err();
2102        assert!(matches!(error, PolyfillError::Network { .. }));
2103    }
2104
2105    #[test]
2106    fn test_client_url_validation() {
2107        let client = create_test_client("https://test.example.com");
2108        assert_eq!(client.base_url, "https://test.example.com");
2109
2110        let client2 = create_test_client("http://localhost:8080");
2111        assert_eq!(client2.base_url, "http://localhost:8080");
2112    }
2113
2114    #[tokio::test]
2115    async fn test_get_midpoints_batch() {
2116        let mut server = Server::new_async().await;
2117        let mock_response = r#"{
2118            "0x123": "0.755",
2119            "0x456": "0.623"
2120        }"#;
2121
2122        let mock = server
2123            .mock("POST", "/midpoints")
2124            .with_header("content-type", "application/json")
2125            .with_status(200)
2126            .with_header("content-type", "application/json")
2127            .with_body(mock_response)
2128            .create_async()
2129            .await;
2130
2131        let client = create_test_client(&server.url());
2132        let token_ids = vec!["0x123".to_string(), "0x456".to_string()];
2133        let result = client.get_midpoints(&token_ids).await;
2134
2135        mock.assert_async().await;
2136        assert!(result.is_ok());
2137        let midpoints = result.unwrap();
2138        assert_eq!(midpoints.len(), 2);
2139        assert_eq!(
2140            midpoints.get("0x123").unwrap(),
2141            &Decimal::from_str("0.755").unwrap()
2142        );
2143        assert_eq!(
2144            midpoints.get("0x456").unwrap(),
2145            &Decimal::from_str("0.623").unwrap()
2146        );
2147    }
2148
2149    #[test]
2150    fn test_client_configuration() {
2151        let client = create_test_client("https://test.example.com");
2152
2153        // Test initial state
2154        assert!(client.signer.is_none());
2155        assert!(client.api_creds.is_none());
2156
2157        // Test with auth
2158        let auth_client = create_test_client_with_auth("https://test.example.com");
2159        assert!(auth_client.signer.is_some());
2160        assert_eq!(auth_client.chain_id, 137);
2161    }
2162
2163    #[tokio::test]
2164    async fn test_get_ok() {
2165        let mut server = Server::new_async().await;
2166        let mock_response = r#"{"status": "ok"}"#;
2167
2168        let mock = server
2169            .mock("GET", "/ok")
2170            .with_header("content-type", "application/json")
2171            .with_status(200)
2172            .with_body(mock_response)
2173            .create_async()
2174            .await;
2175
2176        let client = create_test_client(&server.url());
2177        let result = client.get_ok().await;
2178
2179        mock.assert_async().await;
2180        assert!(result);
2181    }
2182
2183    #[tokio::test]
2184    async fn test_get_prices_batch() {
2185        let mut server = Server::new_async().await;
2186        let mock_response = r#"{
2187            "0x123": {
2188                "BUY": "0.755",
2189                "SELL": "0.745"
2190            },
2191            "0x456": {
2192                "BUY": "0.623",
2193                "SELL": "0.613"
2194            }
2195        }"#;
2196
2197        let mock = server
2198            .mock("POST", "/prices")
2199            .with_header("content-type", "application/json")
2200            .with_status(200)
2201            .with_body(mock_response)
2202            .create_async()
2203            .await;
2204
2205        let client = create_test_client(&server.url());
2206        let book_params = vec![
2207            crate::types::BookParams {
2208                token_id: "0x123".to_string(),
2209                side: Side::BUY,
2210            },
2211            crate::types::BookParams {
2212                token_id: "0x456".to_string(),
2213                side: Side::SELL,
2214            },
2215        ];
2216        let result = client.get_prices(&book_params).await;
2217
2218        mock.assert_async().await;
2219        assert!(result.is_ok());
2220        let prices = result.unwrap();
2221        assert_eq!(prices.len(), 2);
2222        assert!(prices.contains_key("0x123"));
2223        assert!(prices.contains_key("0x456"));
2224    }
2225
2226    #[tokio::test]
2227    async fn test_get_server_time() {
2228        let mut server = Server::new_async().await;
2229        let mock_response = "1234567890"; // Plain text response
2230
2231        let mock = server
2232            .mock("GET", "/time")
2233            .with_status(200)
2234            .with_body(mock_response)
2235            .create_async()
2236            .await;
2237
2238        let client = create_test_client(&server.url());
2239        let result = client.get_server_time().await;
2240
2241        mock.assert_async().await;
2242        assert!(result.is_ok());
2243        let timestamp = result.unwrap();
2244        assert_eq!(timestamp, 1234567890);
2245    }
2246
2247    #[tokio::test]
2248    async fn test_create_or_derive_api_key() {
2249        let mut server = Server::new_async().await;
2250        let mock_response = r#"{
2251            "apiKey": "test-api-key-123",
2252            "secret": "test-secret-456",
2253            "passphrase": "test-passphrase"
2254        }"#;
2255
2256        // Mock both create and derive endpoints since the method tries both
2257        let create_mock = server
2258            .mock("POST", "/auth/api-key")
2259            .with_header("content-type", "application/json")
2260            .with_status(200)
2261            .with_body(mock_response)
2262            .create_async()
2263            .await;
2264
2265        let client = create_test_client_with_auth(&server.url());
2266        let result = client.create_or_derive_api_key(None).await;
2267
2268        create_mock.assert_async().await;
2269        assert!(result.is_ok());
2270        let api_creds = result.unwrap();
2271        assert_eq!(api_creds.api_key, "test-api-key-123");
2272    }
2273    #[tokio::test]
2274    async fn test_get_order_books_batch() {
2275        let mut server = Server::new_async().await;
2276        let mock_response = r#"[
2277            {
2278                "market": "0x123",
2279                "asset_id": "0x123",
2280                "hash": "test-hash",
2281                "timestamp": "1234567890",
2282                "bids": [{"price": "0.75", "size": "100.0"}],
2283                "asks": [{"price": "0.76", "size": "50.0"}]
2284            }
2285        ]"#;
2286
2287        let mock = server
2288            .mock("POST", "/books")
2289            .with_header("content-type", "application/json")
2290            .with_status(200)
2291            .with_body(mock_response)
2292            .create_async()
2293            .await;
2294
2295        let client = create_test_client(&server.url());
2296        let token_ids = vec!["0x123".to_string()];
2297        let result = client.get_order_books(&token_ids).await;
2298
2299        mock.assert_async().await;
2300        if let Err(e) = &result {
2301            println!("Error: {:?}", e);
2302        }
2303        assert!(result.is_ok());
2304        let books = result.unwrap();
2305        assert_eq!(books.len(), 1);
2306    }
2307
2308    #[tokio::test]
2309    async fn test_order_args_creation() {
2310        // Test OrderArgs creation and default values
2311        let order_args = ClientOrderArgs::new(
2312            "0x123",
2313            Decimal::from_str("0.75").unwrap(),
2314            Decimal::from_str("100.0").unwrap(),
2315            Side::BUY,
2316        );
2317
2318        assert_eq!(order_args.token_id, "0x123");
2319        assert_eq!(order_args.price, Decimal::from_str("0.75").unwrap());
2320        assert_eq!(order_args.size, Decimal::from_str("100.0").unwrap());
2321        assert_eq!(order_args.side, Side::BUY);
2322
2323        // Test default
2324        let default_args = ClientOrderArgs::default();
2325        assert_eq!(default_args.token_id, "");
2326        assert_eq!(default_args.price, Decimal::ZERO);
2327        assert_eq!(default_args.size, Decimal::ZERO);
2328        assert_eq!(default_args.side, Side::BUY);
2329    }
2330}