polyfill_rs/
client.rs

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