Skip to main content

polyfill_rs/
client.rs

1//! High-performance Rust client for Polymarket
2//!
3//! This module provides a production-ready client for interacting with
4//! Polymarket, optimized for high-frequency trading environments.
5
6use crate::auth::{create_l1_headers, create_l2_headers};
7use crate::errors::{PolyfillError, Result};
8use crate::http_config::{
9    create_colocated_client, create_internet_client, create_optimized_client, prewarm_connections,
10};
11use crate::types::{OrderOptions, PostOrder, SignedOrderRequest};
12use alloy_primitives::U256;
13use alloy_signer_local::PrivateKeySigner;
14use reqwest::header::HeaderName;
15use reqwest::Client;
16use reqwest::{Method, RequestBuilder};
17use rust_decimal::prelude::FromPrimitive;
18use rust_decimal::Decimal;
19use serde_json::Value;
20use std::str::FromStr;
21
22// Re-export types for compatibility
23pub use crate::types::{ApiCredentials as ApiCreds, OrderType, Side};
24
25// Compatibility types
26#[derive(Debug)]
27pub struct OrderArgs {
28    pub token_id: String,
29    pub price: Decimal,
30    pub size: Decimal,
31    pub side: Side,
32}
33
34impl OrderArgs {
35    pub fn new(token_id: &str, price: Decimal, size: Decimal, side: Side) -> Self {
36        Self {
37            token_id: token_id.to_string(),
38            price,
39            size,
40            side,
41        }
42    }
43}
44
45impl Default for OrderArgs {
46    fn default() -> Self {
47        Self {
48            token_id: "".to_string(),
49            price: Decimal::ZERO,
50            size: Decimal::ZERO,
51            side: Side::BUY,
52        }
53    }
54}
55
56/// Main client for interacting with Polymarket API
57pub struct ClobClient {
58    pub http_client: Client,
59    pub base_url: String,
60    chain_id: u64,
61    signer: Option<PrivateKeySigner>,
62    api_creds: Option<ApiCreds>,
63    order_builder: Option<crate::orders::OrderBuilder>,
64    #[allow(dead_code)]
65    dns_cache: Option<std::sync::Arc<crate::dns_cache::DnsCache>>,
66    #[allow(dead_code)]
67    connection_manager: Option<std::sync::Arc<crate::connection_manager::ConnectionManager>>,
68    #[allow(dead_code)]
69    buffer_pool: std::sync::Arc<crate::buffer_pool::BufferPool>,
70}
71
72impl ClobClient {
73    /// Create a new client with optimized HTTP/2 settings (benchmarked 11.4% faster)
74    /// Now includes DNS caching, connection management, and buffer pooling
75    pub fn new(host: &str) -> Self {
76        // Benchmarked optimal configuration: 512KB stream window
77        // Results: 309.3ms vs 349ms baseline (11.4% improvement)
78        let optimized_client = reqwest::ClientBuilder::new()
79            // Avoid reading OS proxy settings (can be slow and/or unavailable in some sandboxed envs)
80            .no_proxy()
81            .http2_adaptive_window(true)
82            .http2_initial_stream_window_size(512 * 1024) // 512KB - empirically optimal
83            .tcp_nodelay(true)
84            .pool_max_idle_per_host(10)
85            .pool_idle_timeout(std::time::Duration::from_secs(90))
86            .build()
87            .unwrap_or_else(|_| {
88                reqwest::ClientBuilder::new()
89                    .no_proxy()
90                    .build()
91                    .expect("Failed to build reqwest client")
92            });
93
94        // Initialize DNS cache and pre-warm it
95        let dns_cache = tokio::runtime::Handle::try_current().ok().and_then(|_| {
96            tokio::task::block_in_place(|| {
97                tokio::runtime::Handle::current().block_on(async {
98                    let cache = crate::dns_cache::DnsCache::new().await.ok()?;
99                    let hostname = host
100                        .trim_start_matches("https://")
101                        .trim_start_matches("http://")
102                        .split('/')
103                        .next()?;
104                    cache.prewarm(hostname).await.ok()?;
105                    Some(std::sync::Arc::new(cache))
106                })
107            })
108        });
109
110        // Initialize connection manager
111        let connection_manager = Some(std::sync::Arc::new(
112            crate::connection_manager::ConnectionManager::new(
113                optimized_client.clone(),
114                host.to_string(),
115            ),
116        ));
117
118        // Initialize buffer pool (512KB buffers, pool of 10)
119        let buffer_pool = std::sync::Arc::new(crate::buffer_pool::BufferPool::new(512 * 1024, 10));
120
121        // Pre-warm buffer pool with 3 buffers
122        let pool_clone = buffer_pool.clone();
123        if let Ok(_handle) = tokio::runtime::Handle::try_current() {
124            tokio::spawn(async move {
125                pool_clone.prewarm(3).await;
126            });
127        }
128
129        Self {
130            http_client: optimized_client,
131            base_url: host.to_string(),
132            chain_id: 137, // Default to Polygon
133            signer: None,
134            api_creds: None,
135            order_builder: None,
136            dns_cache,
137            connection_manager,
138            buffer_pool,
139        }
140    }
141
142    /// Create a client optimized for co-located environments
143    pub fn new_colocated(host: &str) -> Self {
144        let http_client = create_colocated_client().unwrap_or_else(|_| {
145            reqwest::ClientBuilder::new()
146                .no_proxy()
147                .build()
148                .expect("Failed to build reqwest client")
149        });
150
151        let connection_manager = Some(std::sync::Arc::new(
152            crate::connection_manager::ConnectionManager::new(
153                http_client.clone(),
154                host.to_string(),
155            ),
156        ));
157        let buffer_pool = std::sync::Arc::new(crate::buffer_pool::BufferPool::new(512 * 1024, 10));
158
159        Self {
160            http_client,
161            base_url: host.to_string(),
162            chain_id: 137,
163            signer: None,
164            api_creds: None,
165            order_builder: None,
166            dns_cache: None,
167            connection_manager,
168            buffer_pool,
169        }
170    }
171
172    /// Create a client optimized for internet connections
173    pub fn new_internet(host: &str) -> Self {
174        let http_client = create_internet_client().unwrap_or_else(|_| {
175            reqwest::ClientBuilder::new()
176                .no_proxy()
177                .build()
178                .expect("Failed to build reqwest client")
179        });
180
181        let connection_manager = Some(std::sync::Arc::new(
182            crate::connection_manager::ConnectionManager::new(
183                http_client.clone(),
184                host.to_string(),
185            ),
186        ));
187        let buffer_pool = std::sync::Arc::new(crate::buffer_pool::BufferPool::new(512 * 1024, 10));
188
189        Self {
190            http_client,
191            base_url: host.to_string(),
192            chain_id: 137,
193            signer: None,
194            api_creds: None,
195            order_builder: None,
196            dns_cache: None,
197            connection_manager,
198            buffer_pool,
199        }
200    }
201
202    /// Create a client with L1 headers (for authentication)
203    pub fn with_l1_headers(host: &str, private_key: &str, chain_id: u64) -> Self {
204        let signer = private_key
205            .parse::<PrivateKeySigner>()
206            .expect("Invalid private key");
207
208        let order_builder = crate::orders::OrderBuilder::new(signer.clone(), None, None);
209
210        let http_client = create_optimized_client().unwrap_or_else(|_| {
211            reqwest::ClientBuilder::new()
212                .no_proxy()
213                .build()
214                .expect("Failed to build reqwest client")
215        });
216
217        // Initialize infrastructure modules
218        let dns_cache = None; // Skip DNS cache for simplicity in this constructor
219        let connection_manager = Some(std::sync::Arc::new(
220            crate::connection_manager::ConnectionManager::new(
221                http_client.clone(),
222                host.to_string(),
223            ),
224        ));
225        let buffer_pool = std::sync::Arc::new(crate::buffer_pool::BufferPool::new(512 * 1024, 10));
226
227        Self {
228            http_client,
229            base_url: host.to_string(),
230            chain_id,
231            signer: Some(signer),
232            api_creds: None,
233            order_builder: Some(order_builder),
234            dns_cache,
235            connection_manager,
236            buffer_pool,
237        }
238    }
239
240    /// Create a client with L2 headers (for API key authentication)
241    pub fn with_l2_headers(
242        host: &str,
243        private_key: &str,
244        chain_id: u64,
245        api_creds: ApiCreds,
246    ) -> Self {
247        let signer = private_key
248            .parse::<PrivateKeySigner>()
249            .expect("Invalid private key");
250
251        let order_builder = crate::orders::OrderBuilder::new(signer.clone(), None, None);
252
253        let http_client = create_optimized_client().unwrap_or_else(|_| {
254            reqwest::ClientBuilder::new()
255                .no_proxy()
256                .build()
257                .expect("Failed to build reqwest client")
258        });
259
260        // Initialize infrastructure modules
261        let dns_cache = None; // Skip DNS cache for simplicity in this constructor
262        let connection_manager = Some(std::sync::Arc::new(
263            crate::connection_manager::ConnectionManager::new(
264                http_client.clone(),
265                host.to_string(),
266            ),
267        ));
268        let buffer_pool = std::sync::Arc::new(crate::buffer_pool::BufferPool::new(512 * 1024, 10));
269
270        Self {
271            http_client,
272            base_url: host.to_string(),
273            chain_id,
274            signer: Some(signer),
275            api_creds: Some(api_creds),
276            order_builder: Some(order_builder),
277            dns_cache,
278            connection_manager,
279            buffer_pool,
280        }
281    }
282
283    /// Set API credentials
284    pub fn set_api_creds(&mut self, api_creds: ApiCreds) {
285        self.api_creds = Some(api_creds);
286    }
287
288    /// Start background keep-alive to maintain warm connection
289    /// Sends periodic lightweight requests to prevent connection drops
290    pub async fn start_keepalive(&self, interval: std::time::Duration) {
291        if let Some(manager) = &self.connection_manager {
292            manager.start_keepalive(interval).await;
293        }
294    }
295
296    /// Stop keep-alive background task
297    pub async fn stop_keepalive(&self) {
298        if let Some(manager) = &self.connection_manager {
299            manager.stop_keepalive().await;
300        }
301    }
302
303    /// Pre-warm connections to reduce first-request latency
304    pub async fn prewarm_connections(&self) -> Result<()> {
305        prewarm_connections(&self.http_client, &self.base_url)
306            .await
307            .map_err(|e| {
308                PolyfillError::network(format!("Failed to prewarm connections: {}", e), e)
309            })?;
310        Ok(())
311    }
312
313    /// Get the wallet address
314    pub fn get_address(&self) -> Option<String> {
315        use alloy_primitives::hex;
316        self.signer
317            .as_ref()
318            .map(|s| hex::encode_prefixed(s.address().as_slice()))
319    }
320
321    /// Get the collateral token address for the current chain
322    pub fn get_collateral_address(&self) -> Option<String> {
323        let config = crate::orders::get_contract_config(self.chain_id, false)?;
324        Some(config.collateral)
325    }
326
327    /// Get the conditional tokens contract address for the current chain
328    pub fn get_conditional_address(&self) -> Option<String> {
329        let config = crate::orders::get_contract_config(self.chain_id, false)?;
330        Some(config.conditional_tokens)
331    }
332
333    /// Get the exchange contract address for the current chain
334    pub fn get_exchange_address(&self) -> Option<String> {
335        let config = crate::orders::get_contract_config(self.chain_id, false)?;
336        Some(config.exchange)
337    }
338
339    /// Test basic connectivity
340    pub async fn get_ok(&self) -> bool {
341        match self
342            .http_client
343            .get(format!("{}/ok", self.base_url))
344            .send()
345            .await
346        {
347            Ok(response) => response.status().is_success(),
348            Err(_) => false,
349        }
350    }
351
352    /// Get server time
353    pub async fn get_server_time(&self) -> Result<u64> {
354        let response = self
355            .http_client
356            .get(format!("{}/time", self.base_url))
357            .send()
358            .await?;
359
360        if !response.status().is_success() {
361            return Err(PolyfillError::api(
362                response.status().as_u16(),
363                "Failed to get server time",
364            ));
365        }
366
367        let time_text = response.text().await?;
368        let timestamp = time_text
369            .trim()
370            .parse::<u64>()
371            .map_err(|e| PolyfillError::parse(format!("Invalid timestamp format: {}", e), None))?;
372
373        Ok(timestamp)
374    }
375
376    /// Get order book for a token
377    pub async fn get_order_book(&self, token_id: &str) -> Result<OrderBookSummary> {
378        let response = self
379            .http_client
380            .get(format!("{}/book", self.base_url))
381            .query(&[("token_id", token_id)])
382            .send()
383            .await?;
384
385        if !response.status().is_success() {
386            return Err(PolyfillError::api(
387                response.status().as_u16(),
388                "Failed to get order book",
389            ));
390        }
391
392        let order_book: OrderBookSummary = response.json().await?;
393        Ok(order_book)
394    }
395
396    /// Get midpoint for a token
397    pub async fn get_midpoint(&self, token_id: &str) -> Result<MidpointResponse> {
398        let response = self
399            .http_client
400            .get(format!("{}/midpoint", self.base_url))
401            .query(&[("token_id", token_id)])
402            .send()
403            .await?;
404
405        if !response.status().is_success() {
406            return Err(PolyfillError::api(
407                response.status().as_u16(),
408                "Failed to get midpoint",
409            ));
410        }
411
412        let midpoint: MidpointResponse = response.json().await?;
413        Ok(midpoint)
414    }
415
416    /// Get spread for a token
417    pub async fn get_spread(&self, token_id: &str) -> Result<SpreadResponse> {
418        let response = self
419            .http_client
420            .get(format!("{}/spread", self.base_url))
421            .query(&[("token_id", token_id)])
422            .send()
423            .await?;
424
425        if !response.status().is_success() {
426            return Err(PolyfillError::api(
427                response.status().as_u16(),
428                "Failed to get spread",
429            ));
430        }
431
432        let spread: SpreadResponse = response.json().await?;
433        Ok(spread)
434    }
435
436    /// Get spreads for multiple tokens (batch)
437    pub async fn get_spreads(
438        &self,
439        token_ids: &[String],
440    ) -> Result<std::collections::HashMap<String, Decimal>> {
441        let request_data: Vec<std::collections::HashMap<&str, String>> = token_ids
442            .iter()
443            .map(|id| {
444                let mut map = std::collections::HashMap::new();
445                map.insert("token_id", id.clone());
446                map
447            })
448            .collect();
449
450        let response = self
451            .http_client
452            .post(format!("{}/spreads", self.base_url))
453            .json(&request_data)
454            .send()
455            .await?;
456
457        if !response.status().is_success() {
458            return Err(PolyfillError::api(
459                response.status().as_u16(),
460                "Failed to get batch spreads",
461            ));
462        }
463
464        response
465            .json::<std::collections::HashMap<String, Decimal>>()
466            .await
467            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
468    }
469
470    /// Get price for a token and side
471    pub async fn get_price(&self, token_id: &str, side: Side) -> Result<PriceResponse> {
472        let response = self
473            .http_client
474            .get(format!("{}/price", self.base_url))
475            .query(&[("token_id", token_id), ("side", side.as_str())])
476            .send()
477            .await?;
478
479        if !response.status().is_success() {
480            return Err(PolyfillError::api(
481                response.status().as_u16(),
482                "Failed to get price",
483            ));
484        }
485
486        let price: PriceResponse = response.json().await?;
487        Ok(price)
488    }
489
490    fn validate_prices_history_asset_id(asset_id: &str) -> Result<()> {
491        if asset_id.is_empty() {
492            return Err(PolyfillError::validation(
493                "asset_id is required (use the decimal token_id / asset_id)",
494            ));
495        }
496
497        // Common footgun: passing a condition id (0x...) instead of the decimal asset id.
498        if asset_id.starts_with("0x") || asset_id.starts_with("0X") {
499            return Err(PolyfillError::validation(
500                "`/prices-history` expects a decimal token_id/asset_id, not a hex condition_id",
501            ));
502        }
503
504        if !asset_id.as_bytes().iter().all(u8::is_ascii_digit) {
505            return Err(PolyfillError::validation(
506                "asset_id must be a decimal string (token_id / asset_id)",
507            ));
508        }
509
510        Ok(())
511    }
512
513    /// Get price history for a single outcome (`token_id` / `asset_id`) over a fixed interval.
514    ///
515    /// Important: the upstream API query parameter is named `market`, but it expects the
516    /// decimal outcome asset id (not the hex `condition_id`).
517    pub async fn get_prices_history_interval(
518        &self,
519        asset_id: &str,
520        interval: PricesHistoryInterval,
521        fidelity: Option<u32>,
522    ) -> Result<PricesHistoryResponse> {
523        Self::validate_prices_history_asset_id(asset_id)?;
524
525        let mut request = self
526            .http_client
527            .get(format!("{}/prices-history", self.base_url))
528            .query(&[("market", asset_id), ("interval", interval.as_str())]);
529
530        if let Some(fidelity) = fidelity {
531            request = request.query(&[("fidelity", fidelity)]);
532        }
533
534        let response = request.send().await?;
535        if !response.status().is_success() {
536            let status = response.status().as_u16();
537            let body = response.text().await.unwrap_or_default();
538            let message = serde_json::from_str::<Value>(&body)
539                .ok()
540                .and_then(|v| {
541                    v.get("error")
542                        .and_then(Value::as_str)
543                        .map(|s| s.to_string())
544                })
545                .unwrap_or_else(|| {
546                    if body.is_empty() {
547                        "Failed to get prices history".to_string()
548                    } else {
549                        body
550                    }
551                });
552            return Err(PolyfillError::api(status, message));
553        }
554
555        Ok(response.json::<PricesHistoryResponse>().await?)
556    }
557
558    /// Get price history for a single outcome (`token_id` / `asset_id`) over a timestamp range.
559    ///
560    /// `start_ts` and `end_ts` are Unix timestamps (seconds).
561    pub async fn get_prices_history_range(
562        &self,
563        asset_id: &str,
564        start_ts: u64,
565        end_ts: u64,
566        fidelity: Option<u32>,
567    ) -> Result<PricesHistoryResponse> {
568        Self::validate_prices_history_asset_id(asset_id)?;
569
570        if start_ts >= end_ts {
571            return Err(PolyfillError::validation(
572                "start_ts must be < end_ts for prices history",
573            ));
574        }
575
576        let mut request = self
577            .http_client
578            .get(format!("{}/prices-history", self.base_url))
579            .query(&[("market", asset_id)])
580            .query(&[("startTs", start_ts), ("endTs", end_ts)]);
581
582        if let Some(fidelity) = fidelity {
583            request = request.query(&[("fidelity", fidelity)]);
584        }
585
586        let response = request.send().await?;
587        if !response.status().is_success() {
588            let status = response.status().as_u16();
589            let body = response.text().await.unwrap_or_default();
590            let message = serde_json::from_str::<Value>(&body)
591                .ok()
592                .and_then(|v| {
593                    v.get("error")
594                        .and_then(Value::as_str)
595                        .map(|s| s.to_string())
596                })
597                .unwrap_or_else(|| {
598                    if body.is_empty() {
599                        "Failed to get prices history".to_string()
600                    } else {
601                        body
602                    }
603                });
604            return Err(PolyfillError::api(status, message));
605        }
606
607        Ok(response.json::<PricesHistoryResponse>().await?)
608    }
609
610    /// Get tick size for a token
611    pub async fn get_tick_size(&self, token_id: &str) -> Result<Decimal> {
612        let response = self
613            .http_client
614            .get(format!("{}/tick-size", self.base_url))
615            .query(&[("token_id", token_id)])
616            .send()
617            .await?;
618
619        if !response.status().is_success() {
620            return Err(PolyfillError::api(
621                response.status().as_u16(),
622                "Failed to get tick size",
623            ));
624        }
625
626        let tick_size_response: Value = response.json().await?;
627        let tick_size = tick_size_response["minimum_tick_size"]
628            .as_str()
629            .and_then(|s| Decimal::from_str(s).ok())
630            .or_else(|| {
631                tick_size_response["minimum_tick_size"]
632                    .as_f64()
633                    .map(|f| Decimal::from_f64(f).unwrap_or(Decimal::ZERO))
634            })
635            .ok_or_else(|| PolyfillError::parse("Invalid tick size format", None))?;
636
637        Ok(tick_size)
638    }
639
640    /// Get maker fee rate (in bps) for a token
641    pub async fn get_fee_rate_bps(&self, token_id: &str) -> Result<u32> {
642        let response = self
643            .http_client
644            .get(format!("{}/fee-rate", self.base_url))
645            .query(&[("token_id", token_id)])
646            .send()
647            .await
648            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
649
650        if !response.status().is_success() {
651            return Err(PolyfillError::api(
652                response.status().as_u16(),
653                "Failed to get fee rate",
654            ));
655        }
656
657        let fee_rate: crate::types::FeeRateResponse = response
658            .json()
659            .await
660            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?;
661        Ok(fee_rate.fee_rate_bps)
662    }
663
664    /// Create a new API key
665    pub async fn create_api_key(&self, nonce: Option<U256>) -> Result<ApiCreds> {
666        let signer = self
667            .signer
668            .as_ref()
669            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
670
671        let headers = create_l1_headers(signer, nonce)?;
672        let req =
673            self.create_request_with_headers(Method::POST, "/auth/api-key", headers.into_iter());
674
675        let response = req.send().await?;
676        if !response.status().is_success() {
677            return Err(PolyfillError::api(
678                response.status().as_u16(),
679                "Failed to create API key",
680            ));
681        }
682
683        Ok(response.json::<ApiCreds>().await?)
684    }
685
686    /// Derive an existing API key
687    pub async fn derive_api_key(&self, nonce: Option<U256>) -> Result<ApiCreds> {
688        let signer = self
689            .signer
690            .as_ref()
691            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
692
693        let headers = create_l1_headers(signer, nonce)?;
694        let req = self.create_request_with_headers(
695            Method::GET,
696            "/auth/derive-api-key",
697            headers.into_iter(),
698        );
699
700        let response = req.send().await?;
701        if !response.status().is_success() {
702            return Err(PolyfillError::api(
703                response.status().as_u16(),
704                "Failed to derive API key",
705            ));
706        }
707
708        Ok(response.json::<ApiCreds>().await?)
709    }
710
711    /// Create or derive API key (try create first, fallback to derive)
712    pub async fn create_or_derive_api_key(&self, nonce: Option<U256>) -> Result<ApiCreds> {
713        match self.create_api_key(nonce).await {
714            Ok(creds) => Ok(creds),
715            // Only fall back to derive on API status errors (server responded).
716            // Propagate network/parse/internal errors so callers can handle them appropriately.
717            Err(PolyfillError::Api { .. }) => self.derive_api_key(nonce).await,
718            Err(err) => Err(err),
719        }
720    }
721
722    /// Get all API keys for the authenticated user
723    pub async fn get_api_keys(&self) -> Result<Vec<String>> {
724        let signer = self
725            .signer
726            .as_ref()
727            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
728        let api_creds = self
729            .api_creds
730            .as_ref()
731            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
732
733        let method = Method::GET;
734        let endpoint = "/auth/api-keys";
735        let headers =
736            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
737
738        let response = self
739            .http_client
740            .request(method, format!("{}{}", self.base_url, endpoint))
741            .headers(
742                headers
743                    .into_iter()
744                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
745                    .collect(),
746            )
747            .send()
748            .await
749            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
750
751        let api_keys_response: crate::types::ApiKeysResponse = response
752            .json()
753            .await
754            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?;
755
756        Ok(api_keys_response.api_keys)
757    }
758
759    /// Delete the current API key
760    pub async fn delete_api_key(&self) -> Result<String> {
761        let signer = self
762            .signer
763            .as_ref()
764            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
765        let api_creds = self
766            .api_creds
767            .as_ref()
768            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
769
770        let method = Method::DELETE;
771        let endpoint = "/auth/api-key";
772        let headers =
773            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
774
775        let response = self
776            .http_client
777            .request(method, format!("{}{}", self.base_url, endpoint))
778            .headers(
779                headers
780                    .into_iter()
781                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
782                    .collect(),
783            )
784            .send()
785            .await
786            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
787
788        response
789            .text()
790            .await
791            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
792    }
793
794    /// Helper to create request with headers
795    fn create_request_with_headers(
796        &self,
797        method: Method,
798        endpoint: &str,
799        headers: impl Iterator<Item = (&'static str, String)>,
800    ) -> RequestBuilder {
801        let req = self
802            .http_client
803            .request(method, format!("{}{}", &self.base_url, endpoint));
804        headers.fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v))
805    }
806
807    /// Get neg risk for a token
808    pub async fn get_neg_risk(&self, token_id: &str) -> Result<bool> {
809        let response = self
810            .http_client
811            .get(format!("{}/neg-risk", self.base_url))
812            .query(&[("token_id", token_id)])
813            .send()
814            .await?;
815
816        if !response.status().is_success() {
817            return Err(PolyfillError::api(
818                response.status().as_u16(),
819                "Failed to get neg risk",
820            ));
821        }
822
823        let neg_risk_response: Value = response.json().await?;
824        let neg_risk = neg_risk_response["neg_risk"]
825            .as_bool()
826            .ok_or_else(|| PolyfillError::parse("Invalid neg risk format", None))?;
827
828        Ok(neg_risk)
829    }
830
831    /// Resolve tick size for an order
832    async fn resolve_tick_size(
833        &self,
834        token_id: &str,
835        tick_size: Option<Decimal>,
836    ) -> Result<Decimal> {
837        let min_tick_size = self.get_tick_size(token_id).await?;
838
839        match tick_size {
840            None => Ok(min_tick_size),
841            Some(t) => {
842                if t < min_tick_size {
843                    Err(PolyfillError::validation(format!(
844                        "Tick size {} is smaller than min_tick_size {} for token_id: {}",
845                        t, min_tick_size, token_id
846                    )))
847                } else {
848                    Ok(t)
849                }
850            },
851        }
852    }
853
854    /// Get filled order options
855    async fn get_filled_order_options(
856        &self,
857        token_id: &str,
858        options: Option<&OrderOptions>,
859    ) -> Result<OrderOptions> {
860        let (tick_size, neg_risk, fee_rate_bps) = match options {
861            Some(o) => (o.tick_size, o.neg_risk, o.fee_rate_bps),
862            None => (None, None, None),
863        };
864
865        let tick_size = self.resolve_tick_size(token_id, tick_size).await?;
866        let neg_risk = match neg_risk {
867            Some(nr) => nr,
868            None => self.get_neg_risk(token_id).await?,
869        };
870
871        Ok(OrderOptions {
872            tick_size: Some(tick_size),
873            neg_risk: Some(neg_risk),
874            fee_rate_bps,
875        })
876    }
877
878    /// Check if price is in valid range
879    fn is_price_in_range(&self, price: Decimal, tick_size: Decimal) -> bool {
880        let min_price = tick_size;
881        let max_price = Decimal::ONE - tick_size;
882        price >= min_price && price <= max_price
883    }
884
885    /// Create an order
886    pub async fn create_order(
887        &self,
888        order_args: &OrderArgs,
889        expiration: Option<u64>,
890        extras: Option<crate::types::ExtraOrderArgs>,
891        options: Option<&OrderOptions>,
892    ) -> Result<SignedOrderRequest> {
893        let order_builder = self
894            .order_builder
895            .as_ref()
896            .ok_or_else(|| PolyfillError::auth("Order builder not initialized"))?;
897
898        let create_order_options = self
899            .get_filled_order_options(&order_args.token_id, options)
900            .await?;
901
902        let expiration = expiration.unwrap_or(0);
903        let extras = extras.unwrap_or_default();
904
905        if !self.is_price_in_range(
906            order_args.price,
907            create_order_options.tick_size.expect("Should be filled"),
908        ) {
909            return Err(PolyfillError::validation(
910                "Price is not in range of tick_size",
911            ));
912        }
913
914        order_builder.create_order(
915            self.chain_id,
916            order_args,
917            expiration,
918            &extras,
919            &create_order_options,
920        )
921    }
922
923    /// Calculate market price from order book
924    async fn calculate_market_price(
925        &self,
926        token_id: &str,
927        side: Side,
928        amount: Decimal,
929    ) -> Result<Decimal> {
930        let book = self.get_order_book(token_id).await?;
931        let order_builder = self
932            .order_builder
933            .as_ref()
934            .ok_or_else(|| PolyfillError::auth("Order builder not initialized"))?;
935
936        // Convert OrderSummary to BookLevel
937        let levels: Vec<crate::types::BookLevel> = match side {
938            Side::BUY => book
939                .asks
940                .into_iter()
941                .map(|s| crate::types::BookLevel {
942                    price: s.price,
943                    size: s.size,
944                })
945                .collect(),
946            Side::SELL => book
947                .bids
948                .into_iter()
949                .map(|s| crate::types::BookLevel {
950                    price: s.price,
951                    size: s.size,
952                })
953                .collect(),
954        };
955
956        order_builder.calculate_market_price(&levels, amount)
957    }
958
959    /// Create a market order
960    pub async fn create_market_order(
961        &self,
962        order_args: &crate::types::MarketOrderArgs,
963        extras: Option<crate::types::ExtraOrderArgs>,
964        options: Option<&OrderOptions>,
965    ) -> Result<SignedOrderRequest> {
966        let order_builder = self
967            .order_builder
968            .as_ref()
969            .ok_or_else(|| PolyfillError::auth("Order builder not initialized"))?;
970
971        let create_order_options = self
972            .get_filled_order_options(&order_args.token_id, options)
973            .await?;
974
975        let extras = extras.unwrap_or_default();
976        let price = self
977            .calculate_market_price(&order_args.token_id, Side::BUY, order_args.amount)
978            .await?;
979
980        if !self.is_price_in_range(
981            price,
982            create_order_options.tick_size.expect("Should be filled"),
983        ) {
984            return Err(PolyfillError::validation(
985                "Price is not in range of tick_size",
986            ));
987        }
988
989        order_builder.create_market_order(
990            self.chain_id,
991            order_args,
992            price,
993            &extras,
994            &create_order_options,
995        )
996    }
997
998    /// Post an order to the exchange
999    pub async fn post_order(
1000        &self,
1001        order: SignedOrderRequest,
1002        order_type: OrderType,
1003    ) -> Result<Value> {
1004        let signer = self
1005            .signer
1006            .as_ref()
1007            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1008        let api_creds = self
1009            .api_creds
1010            .as_ref()
1011            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1012
1013        // Owner field must reference the credential principal identifier
1014        // to maintain consistency with the authentication context layer
1015        let body = PostOrder::new(order, api_creds.api_key.clone(), order_type);
1016
1017        let headers = create_l2_headers(signer, api_creds, "POST", "/order", Some(&body))?;
1018        let req = self.create_request_with_headers(Method::POST, "/order", headers.into_iter());
1019
1020        let response = req.json(&body).send().await?;
1021        if !response.status().is_success() {
1022            let status = response.status().as_u16();
1023            let body = response.text().await.unwrap_or_default();
1024            let message = if body.is_empty() {
1025                "Failed to post order".to_string()
1026            } else {
1027                format!("Failed to post order: {}", body)
1028            };
1029            return Err(PolyfillError::api(status, message));
1030        }
1031
1032        Ok(response.json::<Value>().await?)
1033    }
1034
1035    /// Create and post an order in one call
1036    pub async fn create_and_post_order(&self, order_args: &OrderArgs) -> Result<Value> {
1037        let order = self.create_order(order_args, None, None, None).await?;
1038        self.post_order(order, OrderType::GTC).await
1039    }
1040
1041    /// Cancel an order
1042    pub async fn cancel(&self, order_id: &str) -> Result<Value> {
1043        let signer = self
1044            .signer
1045            .as_ref()
1046            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1047        let api_creds = self
1048            .api_creds
1049            .as_ref()
1050            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1051
1052        let body = std::collections::HashMap::from([("orderID", order_id)]);
1053
1054        let headers = create_l2_headers(signer, api_creds, "DELETE", "/order", Some(&body))?;
1055        let req = self.create_request_with_headers(Method::DELETE, "/order", headers.into_iter());
1056
1057        let response = req.json(&body).send().await?;
1058        if !response.status().is_success() {
1059            return Err(PolyfillError::api(
1060                response.status().as_u16(),
1061                "Failed to cancel order",
1062            ));
1063        }
1064
1065        Ok(response.json::<Value>().await?)
1066    }
1067
1068    /// Cancel multiple orders
1069    pub async fn cancel_orders(&self, order_ids: &[String]) -> Result<Value> {
1070        let signer = self
1071            .signer
1072            .as_ref()
1073            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1074        let api_creds = self
1075            .api_creds
1076            .as_ref()
1077            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1078
1079        let headers = create_l2_headers(signer, api_creds, "DELETE", "/orders", Some(order_ids))?;
1080        let req = self.create_request_with_headers(Method::DELETE, "/orders", headers.into_iter());
1081
1082        let response = req.json(order_ids).send().await?;
1083        if !response.status().is_success() {
1084            return Err(PolyfillError::api(
1085                response.status().as_u16(),
1086                "Failed to cancel orders",
1087            ));
1088        }
1089
1090        Ok(response.json::<Value>().await?)
1091    }
1092
1093    /// Cancel all orders
1094    pub async fn cancel_all(&self) -> Result<Value> {
1095        let signer = self
1096            .signer
1097            .as_ref()
1098            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1099        let api_creds = self
1100            .api_creds
1101            .as_ref()
1102            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1103
1104        let headers = create_l2_headers::<Value>(signer, api_creds, "DELETE", "/cancel-all", None)?;
1105        let req =
1106            self.create_request_with_headers(Method::DELETE, "/cancel-all", headers.into_iter());
1107
1108        let response = req.send().await?;
1109        if !response.status().is_success() {
1110            return Err(PolyfillError::api(
1111                response.status().as_u16(),
1112                "Failed to cancel all orders",
1113            ));
1114        }
1115
1116        Ok(response.json::<Value>().await?)
1117    }
1118
1119    /// Get open orders with optional filtering
1120    ///
1121    /// This retrieves all open orders for the authenticated user. You can filter by:
1122    /// - Order ID (exact match)
1123    /// - Asset/Token ID (all orders for a specific token)
1124    /// - Market ID (all orders for a specific market)
1125    ///
1126    /// The response includes order status, fill information, and timestamps.
1127    pub async fn get_orders(
1128        &self,
1129        params: Option<&crate::types::OpenOrderParams>,
1130        next_cursor: Option<&str>,
1131    ) -> Result<Vec<crate::types::OpenOrder>> {
1132        let signer = self
1133            .signer
1134            .as_ref()
1135            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1136        let api_creds = self
1137            .api_creds
1138            .as_ref()
1139            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1140
1141        let method = Method::GET;
1142        let endpoint = "/data/orders";
1143        let headers =
1144            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1145
1146        let query_params = match params {
1147            None => Vec::new(),
1148            Some(p) => p.to_query_params(),
1149        };
1150
1151        let mut next_cursor = next_cursor.unwrap_or("MA==").to_string(); // INITIAL_CURSOR
1152        let mut output = Vec::new();
1153
1154        while next_cursor != "LTE=" {
1155            // END_CURSOR
1156            let req = self
1157                .http_client
1158                .request(method.clone(), format!("{}{}", self.base_url, endpoint))
1159                .query(&query_params)
1160                .query(&[("next_cursor", &next_cursor)]);
1161
1162            let r = headers
1163                .clone()
1164                .into_iter()
1165                .fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v));
1166
1167            let resp = r
1168                .send()
1169                .await
1170                .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?
1171                .json::<Value>()
1172                .await
1173                .map_err(|e| {
1174                    PolyfillError::parse(format!("Failed to parse response: {}", e), None)
1175                })?;
1176
1177            let new_cursor = resp["next_cursor"]
1178                .as_str()
1179                .ok_or_else(|| {
1180                    PolyfillError::parse("Failed to parse next cursor".to_string(), None)
1181                })?
1182                .to_owned();
1183
1184            next_cursor = new_cursor;
1185
1186            let results = resp["data"].clone();
1187            let orders =
1188                serde_json::from_value::<Vec<crate::types::OpenOrder>>(results).map_err(|e| {
1189                    PolyfillError::parse(
1190                        format!("Failed to parse data from order response: {}", e),
1191                        None,
1192                    )
1193                })?;
1194            output.extend(orders);
1195        }
1196
1197        Ok(output)
1198    }
1199
1200    /// Get trade history with optional filtering
1201    ///
1202    /// This retrieves historical trades for the authenticated user. You can filter by:
1203    /// - Trade ID (exact match)
1204    /// - Maker address (trades where you were the maker)
1205    /// - Market ID (trades in a specific market)
1206    /// - Asset/Token ID (trades for a specific token)
1207    /// - Time range (before/after timestamps)
1208    ///
1209    /// Trades are returned in reverse chronological order (newest first).
1210    pub async fn get_trades(
1211        &self,
1212        trade_params: Option<&crate::types::TradeParams>,
1213        next_cursor: Option<&str>,
1214    ) -> Result<Vec<Value>> {
1215        let signer = self
1216            .signer
1217            .as_ref()
1218            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1219        let api_creds = self
1220            .api_creds
1221            .as_ref()
1222            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1223
1224        let method = Method::GET;
1225        let endpoint = "/data/trades";
1226        let headers =
1227            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1228
1229        let query_params = match trade_params {
1230            None => Vec::new(),
1231            Some(p) => p.to_query_params(),
1232        };
1233
1234        let mut next_cursor = next_cursor.unwrap_or("MA==").to_string(); // INITIAL_CURSOR
1235        let mut output = Vec::new();
1236
1237        while next_cursor != "LTE=" {
1238            // END_CURSOR
1239            let req = self
1240                .http_client
1241                .request(method.clone(), format!("{}{}", self.base_url, endpoint))
1242                .query(&query_params)
1243                .query(&[("next_cursor", &next_cursor)]);
1244
1245            let r = headers
1246                .clone()
1247                .into_iter()
1248                .fold(req, |r, (k, v)| r.header(HeaderName::from_static(k), v));
1249
1250            let resp = r
1251                .send()
1252                .await
1253                .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?
1254                .json::<Value>()
1255                .await
1256                .map_err(|e| {
1257                    PolyfillError::parse(format!("Failed to parse response: {}", e), None)
1258                })?;
1259
1260            let new_cursor = resp["next_cursor"]
1261                .as_str()
1262                .ok_or_else(|| {
1263                    PolyfillError::parse("Failed to parse next cursor".to_string(), None)
1264                })?
1265                .to_owned();
1266
1267            next_cursor = new_cursor;
1268
1269            let results = resp["data"].clone();
1270            output.push(results);
1271        }
1272
1273        Ok(output)
1274    }
1275
1276    /// Get balance and allowance information for all assets
1277    ///
1278    /// This returns the current balance and allowance for each asset in your account.
1279    /// Balance is how much you own, allowance is how much the exchange can spend on your behalf.
1280    ///
1281    /// You need both balance and allowance to place orders - the exchange needs permission
1282    /// to move your tokens when orders are filled.
1283    pub async fn get_balance_allowance(
1284        &self,
1285        params: Option<crate::types::BalanceAllowanceParams>,
1286    ) -> Result<Value> {
1287        let signer = self
1288            .signer
1289            .as_ref()
1290            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1291        let api_creds = self
1292            .api_creds
1293            .as_ref()
1294            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1295
1296        let mut params = params.unwrap_or_default();
1297        if params.signature_type.is_none() {
1298            params.set_signature_type(
1299                self.order_builder
1300                    .as_ref()
1301                    .expect("OrderBuilder not set")
1302                    .get_sig_type(),
1303            );
1304        }
1305
1306        let query_params = params.to_query_params();
1307
1308        let method = Method::GET;
1309        let endpoint = "/balance-allowance";
1310        let headers =
1311            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1312
1313        let response = self
1314            .http_client
1315            .request(method, format!("{}{}", self.base_url, endpoint))
1316            .headers(
1317                headers
1318                    .into_iter()
1319                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1320                    .collect(),
1321            )
1322            .query(&query_params)
1323            .send()
1324            .await
1325            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1326
1327        response
1328            .json::<Value>()
1329            .await
1330            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1331    }
1332
1333    /// Set up notifications for order fills and other events
1334    ///
1335    /// This configures push notifications so you get alerted when:
1336    /// - Your orders get filled
1337    /// - Your orders get cancelled
1338    /// - Market conditions change significantly
1339    ///
1340    /// The signature proves you own the account and want to receive notifications.
1341    pub async fn get_notifications(&self) -> Result<Value> {
1342        let signer = self
1343            .signer
1344            .as_ref()
1345            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1346        let api_creds = self
1347            .api_creds
1348            .as_ref()
1349            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1350
1351        let method = Method::GET;
1352        let endpoint = "/notifications";
1353        let headers =
1354            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1355
1356        let response = self
1357            .http_client
1358            .request(method, format!("{}{}", self.base_url, endpoint))
1359            .headers(
1360                headers
1361                    .into_iter()
1362                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1363                    .collect(),
1364            )
1365            .query(&[(
1366                "signature_type",
1367                &self
1368                    .order_builder
1369                    .as_ref()
1370                    .expect("OrderBuilder not set")
1371                    .get_sig_type()
1372                    .to_string(),
1373            )])
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    /// Get midpoints for multiple tokens in a single request
1385    ///
1386    /// This is much more efficient than calling get_midpoint() multiple times.
1387    /// Instead of N round trips, you make just 1 request and get all the midpoints back.
1388    ///
1389    /// Midpoints are returned as a HashMap where the key is the token_id and the value
1390    /// is the midpoint price (or None if there's no valid midpoint).
1391    pub async fn get_midpoints(
1392        &self,
1393        token_ids: &[String],
1394    ) -> Result<std::collections::HashMap<String, Decimal>> {
1395        let request_data: Vec<std::collections::HashMap<&str, String>> = token_ids
1396            .iter()
1397            .map(|id| {
1398                let mut map = std::collections::HashMap::new();
1399                map.insert("token_id", id.clone());
1400                map
1401            })
1402            .collect();
1403
1404        let response = self
1405            .http_client
1406            .post(format!("{}/midpoints", self.base_url))
1407            .json(&request_data)
1408            .send()
1409            .await?;
1410
1411        if !response.status().is_success() {
1412            return Err(PolyfillError::api(
1413                response.status().as_u16(),
1414                "Failed to get batch midpoints",
1415            ));
1416        }
1417
1418        let midpoints: std::collections::HashMap<String, Decimal> = response.json().await?;
1419        Ok(midpoints)
1420    }
1421
1422    /// Get bid/ask/mid prices for multiple tokens in a single request
1423    ///
1424    /// This gives you the full price picture for multiple tokens at once.
1425    /// Much more efficient than individual calls, especially when you're tracking
1426    /// a portfolio or comparing multiple markets.
1427    ///
1428    /// Returns bid (best buy price), ask (best sell price), and mid (average) for each token.
1429    pub async fn get_prices(
1430        &self,
1431        book_params: &[crate::types::BookParams],
1432    ) -> Result<std::collections::HashMap<String, std::collections::HashMap<Side, Decimal>>> {
1433        let request_data: Vec<std::collections::HashMap<&str, String>> = book_params
1434            .iter()
1435            .map(|params| {
1436                let mut map = std::collections::HashMap::new();
1437                map.insert("token_id", params.token_id.clone());
1438                map.insert("side", params.side.as_str().to_string());
1439                map
1440            })
1441            .collect();
1442
1443        let response = self
1444            .http_client
1445            .post(format!("{}/prices", self.base_url))
1446            .json(&request_data)
1447            .send()
1448            .await?;
1449
1450        if !response.status().is_success() {
1451            return Err(PolyfillError::api(
1452                response.status().as_u16(),
1453                "Failed to get batch prices",
1454            ));
1455        }
1456
1457        let prices: std::collections::HashMap<String, std::collections::HashMap<Side, Decimal>> =
1458            response.json().await?;
1459        Ok(prices)
1460    }
1461
1462    /// Get order book for multiple tokens (batch) - reference implementation compatible
1463    pub async fn get_order_books(&self, token_ids: &[String]) -> Result<Vec<OrderBookSummary>> {
1464        let request_data: Vec<std::collections::HashMap<&str, String>> = token_ids
1465            .iter()
1466            .map(|id| {
1467                let mut map = std::collections::HashMap::new();
1468                map.insert("token_id", id.clone());
1469                map
1470            })
1471            .collect();
1472
1473        let response = self
1474            .http_client
1475            .post(format!("{}/books", self.base_url))
1476            .json(&request_data)
1477            .send()
1478            .await
1479            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1480
1481        response
1482            .json::<Vec<OrderBookSummary>>()
1483            .await
1484            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1485    }
1486
1487    /// Get single order by ID
1488    pub async fn get_order(&self, order_id: &str) -> Result<crate::types::OpenOrder> {
1489        let signer = self
1490            .signer
1491            .as_ref()
1492            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1493        let api_creds = self
1494            .api_creds
1495            .as_ref()
1496            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1497
1498        let method = Method::GET;
1499        let endpoint = &format!("/data/order/{}", order_id);
1500        let headers =
1501            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1502
1503        let response = self
1504            .http_client
1505            .request(method, format!("{}{}", self.base_url, endpoint))
1506            .headers(
1507                headers
1508                    .into_iter()
1509                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1510                    .collect(),
1511            )
1512            .send()
1513            .await
1514            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1515
1516        response
1517            .json::<crate::types::OpenOrder>()
1518            .await
1519            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1520    }
1521
1522    /// Get last trade price for a token
1523    pub async fn get_last_trade_price(&self, token_id: &str) -> Result<Value> {
1524        let response = self
1525            .http_client
1526            .get(format!("{}/last-trade-price", self.base_url))
1527            .query(&[("token_id", token_id)])
1528            .send()
1529            .await
1530            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1531
1532        response
1533            .json::<Value>()
1534            .await
1535            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1536    }
1537
1538    /// Get last trade prices for multiple tokens
1539    pub async fn get_last_trade_prices(&self, token_ids: &[String]) -> Result<Value> {
1540        let request_data: Vec<std::collections::HashMap<&str, String>> = token_ids
1541            .iter()
1542            .map(|id| {
1543                let mut map = std::collections::HashMap::new();
1544                map.insert("token_id", id.clone());
1545                map
1546            })
1547            .collect();
1548
1549        let response = self
1550            .http_client
1551            .post(format!("{}/last-trades-prices", self.base_url))
1552            .json(&request_data)
1553            .send()
1554            .await
1555            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1556
1557        response
1558            .json::<Value>()
1559            .await
1560            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1561    }
1562
1563    /// Cancel market orders with optional filters
1564    pub async fn cancel_market_orders(
1565        &self,
1566        market: Option<&str>,
1567        asset_id: Option<&str>,
1568    ) -> Result<Value> {
1569        let signer = self
1570            .signer
1571            .as_ref()
1572            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1573        let api_creds = self
1574            .api_creds
1575            .as_ref()
1576            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1577
1578        let method = Method::DELETE;
1579        let endpoint = "/cancel-market-orders";
1580        let body = std::collections::HashMap::from([
1581            ("market", market.unwrap_or("")),
1582            ("asset_id", asset_id.unwrap_or("")),
1583        ]);
1584
1585        let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(&body))?;
1586
1587        let response = self
1588            .http_client
1589            .request(method, format!("{}{}", self.base_url, endpoint))
1590            .headers(
1591                headers
1592                    .into_iter()
1593                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1594                    .collect(),
1595            )
1596            .json(&body)
1597            .send()
1598            .await
1599            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1600
1601        response
1602            .json::<Value>()
1603            .await
1604            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1605    }
1606
1607    /// Drop (delete) notifications by IDs
1608    pub async fn drop_notifications(&self, ids: &[String]) -> Result<Value> {
1609        let signer = self
1610            .signer
1611            .as_ref()
1612            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1613        let api_creds = self
1614            .api_creds
1615            .as_ref()
1616            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1617
1618        let method = Method::DELETE;
1619        let endpoint = "/notifications";
1620        let headers =
1621            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1622
1623        let response = self
1624            .http_client
1625            .request(method, format!("{}{}", self.base_url, endpoint))
1626            .headers(
1627                headers
1628                    .into_iter()
1629                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1630                    .collect(),
1631            )
1632            .query(&[("ids", ids.join(","))])
1633            .send()
1634            .await
1635            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1636
1637        response
1638            .json::<Value>()
1639            .await
1640            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1641    }
1642
1643    /// Update balance allowance
1644    pub async fn update_balance_allowance(
1645        &self,
1646        params: Option<crate::types::BalanceAllowanceParams>,
1647    ) -> Result<Value> {
1648        let signer = self
1649            .signer
1650            .as_ref()
1651            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1652        let api_creds = self
1653            .api_creds
1654            .as_ref()
1655            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1656
1657        let mut params = params.unwrap_or_default();
1658        if params.signature_type.is_none() {
1659            params.set_signature_type(
1660                self.order_builder
1661                    .as_ref()
1662                    .expect("OrderBuilder not set")
1663                    .get_sig_type(),
1664            );
1665        }
1666
1667        let query_params = params.to_query_params();
1668
1669        let method = Method::GET;
1670        let endpoint = "/balance-allowance/update";
1671        let headers =
1672            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1673
1674        let response = self
1675            .http_client
1676            .request(method, format!("{}{}", self.base_url, endpoint))
1677            .headers(
1678                headers
1679                    .into_iter()
1680                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1681                    .collect(),
1682            )
1683            .query(&query_params)
1684            .send()
1685            .await
1686            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1687
1688        response
1689            .json::<Value>()
1690            .await
1691            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1692    }
1693
1694    /// Check if an order is scoring
1695    pub async fn is_order_scoring(&self, order_id: &str) -> Result<bool> {
1696        let signer = self
1697            .signer
1698            .as_ref()
1699            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1700        let api_creds = self
1701            .api_creds
1702            .as_ref()
1703            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1704
1705        let method = Method::GET;
1706        let endpoint = "/order-scoring";
1707        let headers =
1708            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1709
1710        let response = self
1711            .http_client
1712            .request(method, format!("{}{}", self.base_url, endpoint))
1713            .headers(
1714                headers
1715                    .into_iter()
1716                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1717                    .collect(),
1718            )
1719            .query(&[("order_id", order_id)])
1720            .send()
1721            .await
1722            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1723
1724        let result: Value = response
1725            .json()
1726            .await
1727            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))?;
1728
1729        Ok(result["scoring"].as_bool().unwrap_or(false))
1730    }
1731
1732    /// Check if multiple orders are scoring
1733    pub async fn are_orders_scoring(
1734        &self,
1735        order_ids: &[&str],
1736    ) -> Result<std::collections::HashMap<String, bool>> {
1737        let signer = self
1738            .signer
1739            .as_ref()
1740            .ok_or_else(|| PolyfillError::config("Signer not configured"))?;
1741        let api_creds = self
1742            .api_creds
1743            .as_ref()
1744            .ok_or_else(|| PolyfillError::config("API credentials not configured"))?;
1745
1746        let method = Method::POST;
1747        let endpoint = "/orders-scoring";
1748        let headers = create_l2_headers(
1749            signer,
1750            api_creds,
1751            method.as_str(),
1752            endpoint,
1753            Some(order_ids),
1754        )?;
1755
1756        let response = self
1757            .http_client
1758            .request(method, format!("{}{}", self.base_url, endpoint))
1759            .headers(
1760                headers
1761                    .into_iter()
1762                    .map(|(k, v)| (HeaderName::from_static(k), v.parse().unwrap()))
1763                    .collect(),
1764            )
1765            .json(order_ids)
1766            .send()
1767            .await
1768            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1769
1770        response
1771            .json::<std::collections::HashMap<String, bool>>()
1772            .await
1773            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1774    }
1775
1776    // ============================================================================
1777    // RFQ (Market Maker) endpoints
1778    // ============================================================================
1779
1780    /// Create an RFQ request.
1781    pub async fn create_rfq_request(
1782        &self,
1783        request: &crate::types::RfqCreateRequest,
1784    ) -> Result<crate::types::RfqCreateRequestResponse> {
1785        let signer = self
1786            .signer
1787            .as_ref()
1788            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1789        let api_creds = self
1790            .api_creds
1791            .as_ref()
1792            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1793
1794        let method = Method::POST;
1795        let endpoint = "/rfq/request";
1796        let headers =
1797            create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(request))?;
1798
1799        let response = self
1800            .create_request_with_headers(method, endpoint, headers.into_iter())
1801            .json(request)
1802            .send()
1803            .await
1804            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1805
1806        if !response.status().is_success() {
1807            return Err(PolyfillError::api(
1808                response.status().as_u16(),
1809                "Failed to create RFQ request",
1810            ));
1811        }
1812
1813        response
1814            .json()
1815            .await
1816            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1817    }
1818
1819    /// Cancel an RFQ request.
1820    pub async fn cancel_rfq_request(&self, request_id: &str) -> Result<()> {
1821        let signer = self
1822            .signer
1823            .as_ref()
1824            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1825        let api_creds = self
1826            .api_creds
1827            .as_ref()
1828            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1829
1830        let method = Method::DELETE;
1831        let endpoint = "/rfq/request";
1832        let body = crate::types::RfqCancelRequest {
1833            request_id: request_id.to_string(),
1834        };
1835        let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(&body))?;
1836
1837        let response = self
1838            .create_request_with_headers(method, endpoint, headers.into_iter())
1839            .json(&body)
1840            .send()
1841            .await
1842            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1843
1844        if !response.status().is_success() {
1845            return Err(PolyfillError::api(
1846                response.status().as_u16(),
1847                "Failed to cancel RFQ request",
1848            ));
1849        }
1850
1851        Ok(())
1852    }
1853
1854    /// Get RFQ requests (requester).
1855    pub async fn get_rfq_requests(
1856        &self,
1857        params: Option<&crate::types::RfqRequestsParams>,
1858    ) -> Result<crate::types::RfqListResponse<crate::types::RfqRequestData>> {
1859        let signer = self
1860            .signer
1861            .as_ref()
1862            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1863        let api_creds = self
1864            .api_creds
1865            .as_ref()
1866            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1867
1868        let method = Method::GET;
1869        let endpoint = "/rfq/data/requests";
1870        let headers =
1871            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1872
1873        let query_params = params.cloned().unwrap_or_default().to_query_params();
1874
1875        let response = self
1876            .create_request_with_headers(method, endpoint, headers.into_iter())
1877            .query(&query_params)
1878            .send()
1879            .await
1880            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1881
1882        if !response.status().is_success() {
1883            return Err(PolyfillError::api(
1884                response.status().as_u16(),
1885                "Failed to get RFQ requests",
1886            ));
1887        }
1888
1889        response
1890            .json()
1891            .await
1892            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1893    }
1894
1895    /// Create an RFQ quote.
1896    pub async fn create_rfq_quote(
1897        &self,
1898        quote: &crate::types::RfqCreateQuote,
1899    ) -> Result<crate::types::RfqCreateQuoteResponse> {
1900        let signer = self
1901            .signer
1902            .as_ref()
1903            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1904        let api_creds = self
1905            .api_creds
1906            .as_ref()
1907            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1908
1909        let method = Method::POST;
1910        let endpoint = "/rfq/quote";
1911        let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(quote))?;
1912
1913        let response = self
1914            .create_request_with_headers(method, endpoint, headers.into_iter())
1915            .json(quote)
1916            .send()
1917            .await
1918            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1919
1920        if !response.status().is_success() {
1921            return Err(PolyfillError::api(
1922                response.status().as_u16(),
1923                "Failed to create RFQ quote",
1924            ));
1925        }
1926
1927        response
1928            .json()
1929            .await
1930            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
1931    }
1932
1933    /// Cancel an RFQ quote.
1934    pub async fn cancel_rfq_quote(&self, quote_id: &str) -> Result<()> {
1935        let signer = self
1936            .signer
1937            .as_ref()
1938            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1939        let api_creds = self
1940            .api_creds
1941            .as_ref()
1942            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1943
1944        let method = Method::DELETE;
1945        let endpoint = "/rfq/quote";
1946        let body = crate::types::RfqCancelQuote {
1947            quote_id: quote_id.to_string(),
1948        };
1949        let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(&body))?;
1950
1951        let response = self
1952            .create_request_with_headers(method, endpoint, headers.into_iter())
1953            .json(&body)
1954            .send()
1955            .await
1956            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1957
1958        if !response.status().is_success() {
1959            return Err(PolyfillError::api(
1960                response.status().as_u16(),
1961                "Failed to cancel RFQ quote",
1962            ));
1963        }
1964
1965        Ok(())
1966    }
1967
1968    /// Get quotes for the requester.
1969    pub async fn get_rfq_requester_quotes(
1970        &self,
1971        params: Option<&crate::types::RfqQuotesParams>,
1972    ) -> Result<crate::types::RfqListResponse<crate::types::RfqQuoteData>> {
1973        let signer = self
1974            .signer
1975            .as_ref()
1976            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
1977        let api_creds = self
1978            .api_creds
1979            .as_ref()
1980            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
1981
1982        let method = Method::GET;
1983        let endpoint = "/rfq/data/requester/quotes";
1984        let headers =
1985            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
1986
1987        let query_params = params.cloned().unwrap_or_default().to_query_params();
1988
1989        let response = self
1990            .create_request_with_headers(method, endpoint, headers.into_iter())
1991            .query(&query_params)
1992            .send()
1993            .await
1994            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
1995
1996        if !response.status().is_success() {
1997            return Err(PolyfillError::api(
1998                response.status().as_u16(),
1999                "Failed to get RFQ requester quotes",
2000            ));
2001        }
2002
2003        response
2004            .json()
2005            .await
2006            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
2007    }
2008
2009    /// Get quotes for the quoter.
2010    pub async fn get_rfq_quoter_quotes(
2011        &self,
2012        params: Option<&crate::types::RfqQuotesParams>,
2013    ) -> Result<crate::types::RfqListResponse<crate::types::RfqQuoteData>> {
2014        let signer = self
2015            .signer
2016            .as_ref()
2017            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
2018        let api_creds = self
2019            .api_creds
2020            .as_ref()
2021            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
2022
2023        let method = Method::GET;
2024        let endpoint = "/rfq/data/quoter/quotes";
2025        let headers =
2026            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
2027
2028        let query_params = params.cloned().unwrap_or_default().to_query_params();
2029
2030        let response = self
2031            .create_request_with_headers(method, endpoint, headers.into_iter())
2032            .query(&query_params)
2033            .send()
2034            .await
2035            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
2036
2037        if !response.status().is_success() {
2038            return Err(PolyfillError::api(
2039                response.status().as_u16(),
2040                "Failed to get RFQ quoter quotes",
2041            ));
2042        }
2043
2044        response
2045            .json()
2046            .await
2047            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
2048    }
2049
2050    /// Get best quote for a request.
2051    pub async fn get_rfq_best_quote(&self, request_id: &str) -> Result<crate::types::RfqQuoteData> {
2052        let signer = self
2053            .signer
2054            .as_ref()
2055            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
2056        let api_creds = self
2057            .api_creds
2058            .as_ref()
2059            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
2060
2061        let method = Method::GET;
2062        let endpoint = "/rfq/data/best-quote";
2063        let headers =
2064            create_l2_headers::<Value>(signer, api_creds, method.as_str(), endpoint, None)?;
2065
2066        let response = self
2067            .create_request_with_headers(method, endpoint, headers.into_iter())
2068            .query(&[("requestId", request_id)])
2069            .send()
2070            .await
2071            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
2072
2073        if !response.status().is_success() {
2074            return Err(PolyfillError::api(
2075                response.status().as_u16(),
2076                "Failed to get RFQ best quote",
2077            ));
2078        }
2079
2080        response
2081            .json()
2082            .await
2083            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
2084    }
2085
2086    /// Accept the best quote and post the resulting order.
2087    pub async fn accept_rfq_quote(
2088        &self,
2089        body: &crate::types::RfqOrderExecutionRequest,
2090    ) -> Result<()> {
2091        let signer = self
2092            .signer
2093            .as_ref()
2094            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
2095        let api_creds = self
2096            .api_creds
2097            .as_ref()
2098            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
2099
2100        let method = Method::POST;
2101        let endpoint = "/rfq/request/accept";
2102        let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(body))?;
2103
2104        let response = self
2105            .create_request_with_headers(method, endpoint, headers.into_iter())
2106            .json(body)
2107            .send()
2108            .await
2109            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
2110
2111        if !response.status().is_success() {
2112            return Err(PolyfillError::api(
2113                response.status().as_u16(),
2114                "Failed to accept RFQ quote",
2115            ));
2116        }
2117
2118        Ok(())
2119    }
2120
2121    /// Approve the accepted quote's order (Quoter).
2122    pub async fn approve_rfq_order(
2123        &self,
2124        body: &crate::types::RfqOrderExecutionRequest,
2125    ) -> Result<crate::types::RfqApproveOrderResponse> {
2126        let signer = self
2127            .signer
2128            .as_ref()
2129            .ok_or_else(|| PolyfillError::auth("Signer not set"))?;
2130        let api_creds = self
2131            .api_creds
2132            .as_ref()
2133            .ok_or_else(|| PolyfillError::auth("API credentials not set"))?;
2134
2135        let method = Method::POST;
2136        let endpoint = "/rfq/quote/approve";
2137        let headers = create_l2_headers(signer, api_creds, method.as_str(), endpoint, Some(body))?;
2138
2139        let response = self
2140            .create_request_with_headers(method, endpoint, headers.into_iter())
2141            .json(body)
2142            .send()
2143            .await
2144            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
2145
2146        if !response.status().is_success() {
2147            return Err(PolyfillError::api(
2148                response.status().as_u16(),
2149                "Failed to approve RFQ order",
2150            ));
2151        }
2152
2153        response
2154            .json()
2155            .await
2156            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
2157    }
2158
2159    /// Get sampling markets with pagination
2160    pub async fn get_sampling_markets(
2161        &self,
2162        next_cursor: Option<&str>,
2163    ) -> Result<crate::types::MarketsResponse> {
2164        let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR
2165
2166        let response = self
2167            .http_client
2168            .get(format!("{}/sampling-markets", self.base_url))
2169            .query(&[("next_cursor", next_cursor)])
2170            .send()
2171            .await
2172            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
2173
2174        response
2175            .json::<crate::types::MarketsResponse>()
2176            .await
2177            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
2178    }
2179
2180    /// Get sampling simplified markets with pagination
2181    pub async fn get_sampling_simplified_markets(
2182        &self,
2183        next_cursor: Option<&str>,
2184    ) -> Result<crate::types::SimplifiedMarketsResponse> {
2185        let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR
2186
2187        let response = self
2188            .http_client
2189            .get(format!("{}/sampling-simplified-markets", self.base_url))
2190            .query(&[("next_cursor", next_cursor)])
2191            .send()
2192            .await
2193            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
2194
2195        response
2196            .json::<crate::types::SimplifiedMarketsResponse>()
2197            .await
2198            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
2199    }
2200
2201    /// Get markets with pagination
2202    pub async fn get_markets(
2203        &self,
2204        next_cursor: Option<&str>,
2205    ) -> Result<crate::types::MarketsResponse> {
2206        let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR
2207
2208        let response = self
2209            .http_client
2210            .get(format!("{}/markets", self.base_url))
2211            .query(&[("next_cursor", next_cursor)])
2212            .send()
2213            .await
2214            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
2215
2216        response
2217            .json::<crate::types::MarketsResponse>()
2218            .await
2219            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
2220    }
2221
2222    /// Get simplified markets with pagination
2223    pub async fn get_simplified_markets(
2224        &self,
2225        next_cursor: Option<&str>,
2226    ) -> Result<crate::types::SimplifiedMarketsResponse> {
2227        let next_cursor = next_cursor.unwrap_or("MA=="); // INITIAL_CURSOR
2228
2229        let response = self
2230            .http_client
2231            .get(format!("{}/simplified-markets", self.base_url))
2232            .query(&[("next_cursor", next_cursor)])
2233            .send()
2234            .await
2235            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
2236
2237        response
2238            .json::<crate::types::SimplifiedMarketsResponse>()
2239            .await
2240            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
2241    }
2242
2243    /// Get single market by condition ID
2244    pub async fn get_market(&self, condition_id: &str) -> Result<crate::types::Market> {
2245        let response = self
2246            .http_client
2247            .get(format!("{}/markets/{}", self.base_url, condition_id))
2248            .send()
2249            .await
2250            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
2251
2252        response
2253            .json::<crate::types::Market>()
2254            .await
2255            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
2256    }
2257
2258    /// Get market trades events
2259    pub async fn get_market_trades_events(&self, condition_id: &str) -> Result<Value> {
2260        let response = self
2261            .http_client
2262            .get(format!(
2263                "{}/live-activity/events/{}",
2264                self.base_url, condition_id
2265            ))
2266            .send()
2267            .await
2268            .map_err(|e| PolyfillError::network(format!("Request failed: {}", e), e))?;
2269
2270        response
2271            .json::<Value>()
2272            .await
2273            .map_err(|e| PolyfillError::parse(format!("Failed to parse response: {}", e), None))
2274    }
2275}
2276
2277// Re-export types from the canonical location in types.rs
2278pub use crate::types::{
2279    ExtraOrderArgs, Market, MarketOrderArgs, MarketsResponse, MidpointResponse, NegRiskResponse,
2280    OrderBookSummary, OrderSummary, PriceResponse, PricesHistoryInterval, PricesHistoryResponse,
2281    Rewards, SpreadResponse, TickSizeResponse, Token,
2282};
2283
2284// Compatibility types that need to stay in client.rs
2285#[derive(Debug, Default)]
2286pub struct CreateOrderOptions {
2287    pub tick_size: Option<Decimal>,
2288    pub neg_risk: Option<bool>,
2289}
2290
2291// Re-export for compatibility
2292pub type PolyfillClient = ClobClient;
2293
2294#[cfg(test)]
2295mod tests {
2296    use super::{ClobClient, OrderArgs as ClientOrderArgs};
2297    use crate::types::{
2298        PricesHistoryInterval, RfqCreateQuote, RfqCreateRequest, RfqOrderExecutionRequest,
2299        RfqQuotesParams, RfqRequestsParams, Side,
2300    };
2301    use crate::{ApiCredentials, PolyfillError};
2302    use mockito::{Matcher, Server};
2303    use rust_decimal::Decimal;
2304    use serde_json::json;
2305    use std::str::FromStr;
2306    use tokio;
2307
2308    fn create_test_client(base_url: &str) -> ClobClient {
2309        ClobClient::new(base_url)
2310    }
2311
2312    fn create_test_client_with_auth(base_url: &str) -> ClobClient {
2313        ClobClient::with_l1_headers(
2314            base_url,
2315            "0x1234567890123456789012345678901234567890123456789012345678901234",
2316            137,
2317        )
2318    }
2319
2320    fn create_test_client_with_l2_auth(base_url: &str) -> ClobClient {
2321        let api_creds = ApiCredentials {
2322            api_key: "test_key".to_string(),
2323            // URL-safe base64 so HMAC header generation succeeds.
2324            secret: "dGVzdF9zZWNyZXRfa2V5XzEyMzQ1".to_string(),
2325            passphrase: "test_passphrase".to_string(),
2326        };
2327
2328        ClobClient::with_l2_headers(
2329            base_url,
2330            "0x1234567890123456789012345678901234567890123456789012345678901234",
2331            137,
2332            api_creds,
2333        )
2334    }
2335
2336    #[tokio::test(flavor = "multi_thread")]
2337    async fn test_client_creation() {
2338        let client = create_test_client("https://test.example.com");
2339        assert_eq!(client.base_url, "https://test.example.com");
2340        assert!(client.signer.is_none());
2341        assert!(client.api_creds.is_none());
2342    }
2343
2344    #[tokio::test(flavor = "multi_thread")]
2345    async fn test_client_with_l1_headers() {
2346        let client = create_test_client_with_auth("https://test.example.com");
2347        assert_eq!(client.base_url, "https://test.example.com");
2348        assert!(client.signer.is_some());
2349        assert_eq!(client.chain_id, 137);
2350    }
2351
2352    #[tokio::test(flavor = "multi_thread")]
2353    async fn test_client_with_l2_headers() {
2354        let api_creds = ApiCredentials {
2355            api_key: "test_key".to_string(),
2356            secret: "test_secret".to_string(),
2357            passphrase: "test_passphrase".to_string(),
2358        };
2359
2360        let client = ClobClient::with_l2_headers(
2361            "https://test.example.com",
2362            "0x1234567890123456789012345678901234567890123456789012345678901234",
2363            137,
2364            api_creds.clone(),
2365        );
2366
2367        assert_eq!(client.base_url, "https://test.example.com");
2368        assert!(client.signer.is_some());
2369        assert!(client.api_creds.is_some());
2370        assert_eq!(client.chain_id, 137);
2371    }
2372
2373    #[tokio::test(flavor = "multi_thread")]
2374    async fn test_set_api_creds() {
2375        let mut client = create_test_client("https://test.example.com");
2376        assert!(client.api_creds.is_none());
2377
2378        let api_creds = ApiCredentials {
2379            api_key: "test_key".to_string(),
2380            secret: "test_secret".to_string(),
2381            passphrase: "test_passphrase".to_string(),
2382        };
2383
2384        client.set_api_creds(api_creds.clone());
2385        assert!(client.api_creds.is_some());
2386        assert_eq!(client.api_creds.unwrap().api_key, "test_key");
2387    }
2388
2389    #[tokio::test(flavor = "multi_thread")]
2390    async fn test_get_sampling_markets_success() {
2391        let mut server = Server::new_async().await;
2392        let mock_response = r#"{
2393            "limit": 10,
2394            "count": 2, 
2395            "next_cursor": null,
2396            "data": [
2397                {
2398                    "condition_id": "0x123",
2399                    "tokens": [
2400                        {"token_id": "0x456", "outcome": "Yes", "price": 0.5, "winner": false},
2401                        {"token_id": "0x789", "outcome": "No", "price": 0.5, "winner": false}
2402                    ],
2403                    "rewards": {
2404                        "rates": null,
2405                        "min_size": 1.0,
2406                        "max_spread": 0.1,
2407                        "event_start_date": null,
2408                        "event_end_date": null,
2409                        "in_game_multiplier": null,
2410                        "reward_epoch": null
2411                    },
2412                    "min_incentive_size": null,
2413                    "max_incentive_spread": null,
2414                    "active": true,
2415                    "closed": false,
2416                    "question_id": "0x123",
2417                    "minimum_order_size": 1.0,
2418                    "minimum_tick_size": 0.01,
2419                    "description": "Test market",
2420                    "category": "test",
2421                    "end_date_iso": null,
2422                    "game_start_time": null,
2423                    "question": "Will this test pass?",
2424                    "market_slug": "test-market",
2425                    "seconds_delay": 0,
2426                    "icon": "",
2427                    "fpmm": ""
2428                }
2429            ]
2430        }"#;
2431
2432        let mock = server
2433            .mock("GET", "/sampling-markets")
2434            .match_query(Matcher::UrlEncoded("next_cursor".into(), "MA==".into()))
2435            .with_status(200)
2436            .with_header("content-type", "application/json")
2437            .with_body(mock_response)
2438            .create_async()
2439            .await;
2440
2441        let client = create_test_client(&server.url());
2442        let result = client.get_sampling_markets(None).await;
2443
2444        mock.assert_async().await;
2445        assert!(result.is_ok());
2446        let markets = result.unwrap();
2447        assert_eq!(markets.data.len(), 1);
2448        assert_eq!(markets.data[0].question, "Will this test pass?");
2449    }
2450
2451    #[tokio::test(flavor = "multi_thread")]
2452    async fn test_get_sampling_markets_with_cursor() {
2453        let mut server = Server::new_async().await;
2454        let mock_response = r#"{
2455            "limit": 5,
2456            "count": 0,
2457            "next_cursor": null,
2458            "data": []
2459        }"#;
2460
2461        let mock = server
2462            .mock("GET", "/sampling-markets")
2463            .match_query(Matcher::AllOf(vec![Matcher::UrlEncoded(
2464                "next_cursor".into(),
2465                "test_cursor".into(),
2466            )]))
2467            .with_status(200)
2468            .with_header("content-type", "application/json")
2469            .with_body(mock_response)
2470            .create_async()
2471            .await;
2472
2473        let client = create_test_client(&server.url());
2474        let result = client.get_sampling_markets(Some("test_cursor")).await;
2475
2476        mock.assert_async().await;
2477        assert!(result.is_ok());
2478        let markets = result.unwrap();
2479        assert_eq!(markets.data.len(), 0);
2480    }
2481
2482    #[tokio::test(flavor = "multi_thread")]
2483    async fn test_get_order_book_success() {
2484        let mut server = Server::new_async().await;
2485        let mock_response = r#"{
2486            "market": "0x123",
2487            "asset_id": "0x123",
2488            "hash": "0xabc123",
2489            "timestamp": "1234567890",
2490            "bids": [
2491                {"price": "0.75", "size": "100.0"}
2492            ],
2493            "asks": [
2494                {"price": "0.76", "size": "50.0"}
2495            ],
2496            "min_order_size": "1",
2497            "neg_risk": false,
2498            "tick_size": "0.01",
2499            "last_trade_price": "0.755"
2500        }"#;
2501
2502        let mock = server
2503            .mock("GET", "/book")
2504            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
2505            .with_status(200)
2506            .with_header("content-type", "application/json")
2507            .with_body(mock_response)
2508            .create_async()
2509            .await;
2510
2511        let client = create_test_client(&server.url());
2512        let result = client.get_order_book("0x123").await;
2513
2514        mock.assert_async().await;
2515        assert!(result.is_ok());
2516        let book = result.unwrap();
2517        assert_eq!(book.market, "0x123");
2518        assert_eq!(book.bids.len(), 1);
2519        assert_eq!(book.asks.len(), 1);
2520        assert_eq!(book.min_order_size, Decimal::from_str("1").unwrap());
2521        assert!(!book.neg_risk);
2522        assert_eq!(book.tick_size, Decimal::from_str("0.01").unwrap());
2523        assert_eq!(
2524            book.last_trade_price,
2525            Some(Decimal::from_str("0.755").unwrap())
2526        );
2527    }
2528
2529    #[tokio::test(flavor = "multi_thread")]
2530    async fn test_get_midpoint_success() {
2531        let mut server = Server::new_async().await;
2532        let mock_response = r#"{
2533            "mid": "0.755"
2534        }"#;
2535
2536        let mock = server
2537            .mock("GET", "/midpoint")
2538            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
2539            .with_status(200)
2540            .with_header("content-type", "application/json")
2541            .with_body(mock_response)
2542            .create_async()
2543            .await;
2544
2545        let client = create_test_client(&server.url());
2546        let result = client.get_midpoint("0x123").await;
2547
2548        mock.assert_async().await;
2549        assert!(result.is_ok());
2550        let response = result.unwrap();
2551        assert_eq!(response.mid, Decimal::from_str("0.755").unwrap());
2552    }
2553
2554    #[tokio::test(flavor = "multi_thread")]
2555    async fn test_get_spread_success() {
2556        let mut server = Server::new_async().await;
2557        let mock_response = r#"{
2558            "spread": "0.01"
2559        }"#;
2560
2561        let mock = server
2562            .mock("GET", "/spread")
2563            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
2564            .with_status(200)
2565            .with_header("content-type", "application/json")
2566            .with_body(mock_response)
2567            .create_async()
2568            .await;
2569
2570        let client = create_test_client(&server.url());
2571        let result = client.get_spread("0x123").await;
2572
2573        mock.assert_async().await;
2574        assert!(result.is_ok());
2575        let response = result.unwrap();
2576        assert_eq!(response.spread, Decimal::from_str("0.01").unwrap());
2577    }
2578
2579    #[tokio::test(flavor = "multi_thread")]
2580    async fn test_get_price_success() {
2581        let mut server = Server::new_async().await;
2582        let mock_response = r#"{
2583            "price": "0.76"
2584        }"#;
2585
2586        let mock = server
2587            .mock("GET", "/price")
2588            .match_query(Matcher::AllOf(vec![
2589                Matcher::UrlEncoded("token_id".into(), "0x123".into()),
2590                Matcher::UrlEncoded("side".into(), "BUY".into()),
2591            ]))
2592            .with_status(200)
2593            .with_header("content-type", "application/json")
2594            .with_body(mock_response)
2595            .create_async()
2596            .await;
2597
2598        let client = create_test_client(&server.url());
2599        let result = client.get_price("0x123", Side::BUY).await;
2600
2601        mock.assert_async().await;
2602        assert!(result.is_ok());
2603        let response = result.unwrap();
2604        assert_eq!(response.price, Decimal::from_str("0.76").unwrap());
2605    }
2606
2607    #[tokio::test(flavor = "multi_thread")]
2608    async fn test_get_prices_history_interval_rejects_hex_condition_id() {
2609        let client = create_test_client("https://test.example.com");
2610        let result = client
2611            .get_prices_history_interval("0xdeadbeef", PricesHistoryInterval::OneDay, None)
2612            .await;
2613        assert!(matches!(result, Err(PolyfillError::Validation { .. })));
2614    }
2615
2616    #[tokio::test(flavor = "multi_thread")]
2617    async fn test_get_prices_history_interval_success() {
2618        let mut server = Server::new_async().await;
2619        let mock_response = r#"{"history":[{"t":1}]}"#;
2620
2621        let mock = server
2622            .mock("GET", "/prices-history")
2623            .match_query(Matcher::AllOf(vec![
2624                Matcher::UrlEncoded("market".into(), "12345".into()),
2625                Matcher::UrlEncoded("interval".into(), "1d".into()),
2626                Matcher::UrlEncoded("fidelity".into(), "5".into()),
2627            ]))
2628            .with_status(200)
2629            .with_header("content-type", "application/json")
2630            .with_body(mock_response)
2631            .create_async()
2632            .await;
2633
2634        let client = create_test_client(&server.url());
2635        let response = client
2636            .get_prices_history_interval("12345", PricesHistoryInterval::OneDay, Some(5))
2637            .await
2638            .unwrap();
2639
2640        mock.assert_async().await;
2641        assert_eq!(response.history.len(), 1);
2642    }
2643
2644    #[tokio::test(flavor = "multi_thread")]
2645    async fn test_get_tick_size_success() {
2646        let mut server = Server::new_async().await;
2647        let mock_response = r#"{
2648            "minimum_tick_size": "0.01"
2649        }"#;
2650
2651        let mock = server
2652            .mock("GET", "/tick-size")
2653            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
2654            .with_status(200)
2655            .with_header("content-type", "application/json")
2656            .with_body(mock_response)
2657            .create_async()
2658            .await;
2659
2660        let client = create_test_client(&server.url());
2661        let result = client.get_tick_size("0x123").await;
2662
2663        mock.assert_async().await;
2664        assert!(result.is_ok());
2665        let tick_size = result.unwrap();
2666        assert_eq!(tick_size, Decimal::from_str("0.01").unwrap());
2667    }
2668
2669    #[tokio::test(flavor = "multi_thread")]
2670    async fn test_get_neg_risk_success() {
2671        let mut server = Server::new_async().await;
2672        let mock_response = r#"{
2673            "neg_risk": false
2674        }"#;
2675
2676        let mock = server
2677            .mock("GET", "/neg-risk")
2678            .match_query(Matcher::UrlEncoded("token_id".into(), "0x123".into()))
2679            .with_status(200)
2680            .with_header("content-type", "application/json")
2681            .with_body(mock_response)
2682            .create_async()
2683            .await;
2684
2685        let client = create_test_client(&server.url());
2686        let result = client.get_neg_risk("0x123").await;
2687
2688        mock.assert_async().await;
2689        assert!(result.is_ok());
2690        let neg_risk = result.unwrap();
2691        assert!(!neg_risk);
2692    }
2693
2694    #[tokio::test(flavor = "multi_thread")]
2695    async fn test_api_error_handling() {
2696        let mut server = Server::new_async().await;
2697
2698        let mock = server
2699            .mock("GET", "/book")
2700            .match_query(Matcher::UrlEncoded(
2701                "token_id".into(),
2702                "invalid_token".into(),
2703            ))
2704            .with_status(404)
2705            .with_header("content-type", "application/json")
2706            .with_body(r#"{"error": "Market not found"}"#)
2707            .create_async()
2708            .await;
2709
2710        let client = create_test_client(&server.url());
2711        let result = client.get_order_book("invalid_token").await;
2712
2713        mock.assert_async().await;
2714        assert!(result.is_err());
2715
2716        let error = result.unwrap_err();
2717        // The error should be either Network or Api error
2718        assert!(
2719            matches!(error, PolyfillError::Network { .. })
2720                || matches!(error, PolyfillError::Api { .. })
2721        );
2722    }
2723
2724    #[tokio::test(flavor = "multi_thread")]
2725    async fn test_network_error_handling() {
2726        // Test with invalid URL to simulate network error
2727        let client = create_test_client("http://invalid-host-that-does-not-exist.com");
2728        let result = client.get_order_book("0x123").await;
2729
2730        assert!(result.is_err());
2731        let error = result.unwrap_err();
2732        assert!(matches!(error, PolyfillError::Network { .. }));
2733    }
2734
2735    #[test]
2736    fn test_client_url_validation() {
2737        let client = create_test_client("https://test.example.com");
2738        assert_eq!(client.base_url, "https://test.example.com");
2739
2740        let client2 = create_test_client("http://localhost:8080");
2741        assert_eq!(client2.base_url, "http://localhost:8080");
2742    }
2743
2744    #[tokio::test(flavor = "multi_thread")]
2745    async fn test_get_midpoints_batch() {
2746        let mut server = Server::new_async().await;
2747        let mock_response = r#"{
2748            "0x123": "0.755",
2749            "0x456": "0.623"
2750        }"#;
2751
2752        let mock = server
2753            .mock("POST", "/midpoints")
2754            .with_header("content-type", "application/json")
2755            .with_status(200)
2756            .with_header("content-type", "application/json")
2757            .with_body(mock_response)
2758            .create_async()
2759            .await;
2760
2761        let client = create_test_client(&server.url());
2762        let token_ids = vec!["0x123".to_string(), "0x456".to_string()];
2763        let result = client.get_midpoints(&token_ids).await;
2764
2765        mock.assert_async().await;
2766        assert!(result.is_ok());
2767        let midpoints = result.unwrap();
2768        assert_eq!(midpoints.len(), 2);
2769        assert_eq!(
2770            midpoints.get("0x123").unwrap(),
2771            &Decimal::from_str("0.755").unwrap()
2772        );
2773        assert_eq!(
2774            midpoints.get("0x456").unwrap(),
2775            &Decimal::from_str("0.623").unwrap()
2776        );
2777    }
2778
2779    #[test]
2780    fn test_client_configuration() {
2781        let client = create_test_client("https://test.example.com");
2782
2783        // Test initial state
2784        assert!(client.signer.is_none());
2785        assert!(client.api_creds.is_none());
2786
2787        // Test with auth
2788        let auth_client = create_test_client_with_auth("https://test.example.com");
2789        assert!(auth_client.signer.is_some());
2790        assert_eq!(auth_client.chain_id, 137);
2791    }
2792
2793    #[tokio::test(flavor = "multi_thread")]
2794    async fn test_get_ok() {
2795        let mut server = Server::new_async().await;
2796        let mock_response = r#"{"status": "ok"}"#;
2797
2798        let mock = server
2799            .mock("GET", "/ok")
2800            .with_header("content-type", "application/json")
2801            .with_status(200)
2802            .with_body(mock_response)
2803            .create_async()
2804            .await;
2805
2806        let client = create_test_client(&server.url());
2807        let result = client.get_ok().await;
2808
2809        mock.assert_async().await;
2810        assert!(result);
2811    }
2812
2813    #[tokio::test(flavor = "multi_thread")]
2814    async fn test_get_prices_batch() {
2815        let mut server = Server::new_async().await;
2816        let mock_response = r#"{
2817            "0x123": {
2818                "BUY": "0.755",
2819                "SELL": "0.745"
2820            },
2821            "0x456": {
2822                "BUY": "0.623",
2823                "SELL": "0.613"
2824            }
2825        }"#;
2826
2827        let mock = server
2828            .mock("POST", "/prices")
2829            .with_header("content-type", "application/json")
2830            .with_status(200)
2831            .with_body(mock_response)
2832            .create_async()
2833            .await;
2834
2835        let client = create_test_client(&server.url());
2836        let book_params = vec![
2837            crate::types::BookParams {
2838                token_id: "0x123".to_string(),
2839                side: Side::BUY,
2840            },
2841            crate::types::BookParams {
2842                token_id: "0x456".to_string(),
2843                side: Side::SELL,
2844            },
2845        ];
2846        let result = client.get_prices(&book_params).await;
2847
2848        mock.assert_async().await;
2849        assert!(result.is_ok());
2850        let prices = result.unwrap();
2851        assert_eq!(prices.len(), 2);
2852        assert!(prices.contains_key("0x123"));
2853        assert!(prices.contains_key("0x456"));
2854    }
2855
2856    #[tokio::test(flavor = "multi_thread")]
2857    async fn test_get_server_time() {
2858        let mut server = Server::new_async().await;
2859        let mock_response = "1234567890"; // Plain text response
2860
2861        let mock = server
2862            .mock("GET", "/time")
2863            .with_status(200)
2864            .with_body(mock_response)
2865            .create_async()
2866            .await;
2867
2868        let client = create_test_client(&server.url());
2869        let result = client.get_server_time().await;
2870
2871        mock.assert_async().await;
2872        assert!(result.is_ok());
2873        let timestamp = result.unwrap();
2874        assert_eq!(timestamp, 1234567890);
2875    }
2876
2877    #[tokio::test(flavor = "multi_thread")]
2878    async fn test_create_or_derive_api_key() {
2879        let mut server = Server::new_async().await;
2880        let mock_response = r#"{
2881            "apiKey": "test-api-key-123",
2882            "secret": "test-secret-456",
2883            "passphrase": "test-passphrase"
2884        }"#;
2885
2886        // Mock both create and derive endpoints since the method tries both
2887        let create_mock = server
2888            .mock("POST", "/auth/api-key")
2889            .with_header("content-type", "application/json")
2890            .with_status(200)
2891            .with_body(mock_response)
2892            .create_async()
2893            .await;
2894
2895        let client = create_test_client_with_auth(&server.url());
2896        let result = client.create_or_derive_api_key(None).await;
2897
2898        create_mock.assert_async().await;
2899        assert!(result.is_ok());
2900        let api_creds = result.unwrap();
2901        assert_eq!(api_creds.api_key, "test-api-key-123");
2902    }
2903
2904    #[tokio::test(flavor = "multi_thread")]
2905    async fn test_create_or_derive_api_key_falls_back_on_api_error() {
2906        let mut server = Server::new_async().await;
2907
2908        // Create fails with a status error -> should fall back to derive.
2909        let create_mock = server
2910            .mock("POST", "/auth/api-key")
2911            .with_status(400)
2912            .with_header("content-type", "application/json")
2913            .with_body(r#"{"error":"key exists"}"#)
2914            .create_async()
2915            .await;
2916
2917        let derive_mock = server
2918            .mock("GET", "/auth/derive-api-key")
2919            .with_status(200)
2920            .with_header("content-type", "application/json")
2921            .with_body(
2922                r#"{"apiKey":"derived-api-key","secret":"derived-secret","passphrase":"derived-pass"}"#,
2923            )
2924            .create_async()
2925            .await;
2926
2927        let client = create_test_client_with_auth(&server.url());
2928        let result = client.create_or_derive_api_key(None).await;
2929
2930        create_mock.assert_async().await;
2931        derive_mock.assert_async().await;
2932        assert!(result.is_ok());
2933        assert_eq!(result.unwrap().api_key, "derived-api-key");
2934    }
2935
2936    #[tokio::test(flavor = "multi_thread")]
2937    async fn test_create_or_derive_api_key_does_not_fallback_on_non_api_error() {
2938        let mut server = Server::new_async().await;
2939
2940        // Create returns 200 but with invalid JSON -> not an API status error.
2941        let create_mock = server
2942            .mock("POST", "/auth/api-key")
2943            .with_status(200)
2944            .with_header("content-type", "application/json")
2945            .with_body("not-json")
2946            .create_async()
2947            .await;
2948
2949        // If we incorrectly fall back, this would be called.
2950        let derive_mock = server
2951            .mock("GET", "/auth/derive-api-key")
2952            .with_status(200)
2953            .with_header("content-type", "application/json")
2954            .with_body(
2955                r#"{"apiKey":"derived-api-key","secret":"derived-secret","passphrase":"derived-pass"}"#,
2956            )
2957            .expect(0)
2958            .create_async()
2959            .await;
2960
2961        let client = create_test_client_with_auth(&server.url());
2962        let result = client.create_or_derive_api_key(None).await;
2963
2964        create_mock.assert_async().await;
2965        derive_mock.assert_async().await;
2966        assert!(result.is_err());
2967    }
2968    #[tokio::test(flavor = "multi_thread")]
2969    async fn test_get_order_books_batch() {
2970        let mut server = Server::new_async().await;
2971        let mock_response = r#"[
2972            {
2973                "market": "0x123",
2974                "asset_id": "0x123",
2975                "hash": "test-hash",
2976                "timestamp": "1234567890",
2977                "bids": [{"price": "0.75", "size": "100.0"}],
2978                "asks": [{"price": "0.76", "size": "50.0"}],
2979                "min_order_size": "1",
2980                "neg_risk": false,
2981                "tick_size": "0.01",
2982                "last_trade_price": null
2983            }
2984        ]"#;
2985
2986        let mock = server
2987            .mock("POST", "/books")
2988            .with_header("content-type", "application/json")
2989            .with_status(200)
2990            .with_body(mock_response)
2991            .create_async()
2992            .await;
2993
2994        let client = create_test_client(&server.url());
2995        let token_ids = vec!["0x123".to_string()];
2996        let result = client.get_order_books(&token_ids).await;
2997
2998        mock.assert_async().await;
2999        if let Err(e) = &result {
3000            println!("Error: {:?}", e);
3001        }
3002        assert!(result.is_ok());
3003        let books = result.unwrap();
3004        assert_eq!(books.len(), 1);
3005    }
3006
3007    #[tokio::test(flavor = "multi_thread")]
3008    async fn test_order_args_creation() {
3009        // Test OrderArgs creation and default values
3010        let order_args = ClientOrderArgs::new(
3011            "0x123",
3012            Decimal::from_str("0.75").unwrap(),
3013            Decimal::from_str("100.0").unwrap(),
3014            Side::BUY,
3015        );
3016
3017        assert_eq!(order_args.token_id, "0x123");
3018        assert_eq!(order_args.price, Decimal::from_str("0.75").unwrap());
3019        assert_eq!(order_args.size, Decimal::from_str("100.0").unwrap());
3020        assert_eq!(order_args.side, Side::BUY);
3021
3022        // Test default
3023        let default_args = ClientOrderArgs::default();
3024        assert_eq!(default_args.token_id, "");
3025        assert_eq!(default_args.price, Decimal::ZERO);
3026        assert_eq!(default_args.size, Decimal::ZERO);
3027        assert_eq!(default_args.side, Side::BUY);
3028    }
3029
3030    #[tokio::test(flavor = "multi_thread")]
3031    async fn test_get_fee_rate_bps_success() {
3032        let mut server = Server::new_async().await;
3033        let mock = server
3034            .mock("GET", "/fee-rate")
3035            .match_query(Matcher::UrlEncoded("token_id".into(), "123".into()))
3036            .with_status(200)
3037            .with_header("content-type", "application/json")
3038            .with_body(r#"{"fee_rate_bps":1000}"#)
3039            .create_async()
3040            .await;
3041
3042        let client = create_test_client(&server.url());
3043        let rate = client.get_fee_rate_bps("123").await.unwrap();
3044
3045        mock.assert_async().await;
3046        assert_eq!(rate, 1000);
3047    }
3048
3049    #[tokio::test(flavor = "multi_thread")]
3050    async fn test_rfq_endpoints_happy_path() {
3051        let mut server = Server::new_async().await;
3052
3053        // create_rfq_request
3054        let create_request = RfqCreateRequest {
3055            asset_in: "some_asset_in".to_string(),
3056            asset_out: "some_asset_out".to_string(),
3057            amount_in: "100".to_string(),
3058            amount_out: "200".to_string(),
3059            user_type: 0,
3060        };
3061        let create_request_mock = server
3062            .mock("POST", "/rfq/request")
3063            .match_body(Matcher::JsonString(
3064                json!({
3065                    "assetIn": "some_asset_in",
3066                    "assetOut": "some_asset_out",
3067                    "amountIn": "100",
3068                    "amountOut": "200",
3069                    "userType": 0
3070                })
3071                .to_string(),
3072            ))
3073            .with_status(200)
3074            .with_header("content-type", "application/json")
3075            .with_body(r#"{"requestId":"req123","expiry":1744936318}"#)
3076            .create_async()
3077            .await;
3078
3079        // cancel_rfq_request
3080        let cancel_request_mock = server
3081            .mock("DELETE", "/rfq/request")
3082            .match_body(Matcher::JsonString(r#"{"requestId":"req123"}"#.to_string()))
3083            .with_status(200)
3084            .with_body("OK")
3085            .create_async()
3086            .await;
3087
3088        // get_rfq_requests
3089        let rfq_requests_mock = server
3090            .mock("GET", "/rfq/data/requests")
3091            .match_query(Matcher::AllOf(vec![
3092                Matcher::UrlEncoded("offset".into(), "MA==".into()),
3093                Matcher::UrlEncoded("limit".into(), "10".into()),
3094                Matcher::UrlEncoded("state".into(), "active".into()),
3095                Matcher::UrlEncoded("requestIds[]".into(), "req123".into()),
3096                Matcher::UrlEncoded("markets[]".into(), "some_market".into()),
3097            ]))
3098            .with_status(200)
3099            .with_header("content-type", "application/json")
3100            .with_body(
3101                r#"{
3102                    "data": [{
3103                        "requestId": "req123",
3104                        "userAddress": "0xabc",
3105                        "proxyAddress": "0xdef",
3106                        "condition": "some_condition_id",
3107                        "token": "some_token_id",
3108                        "complement": "some_complement",
3109                        "side": "BUY",
3110                        "sizeIn": 100,
3111                        "sizeOut": 200,
3112                        "price": 0.5,
3113                        "state": "active",
3114                        "expiry": 1744936318
3115                    }],
3116                    "next_cursor": "MA==",
3117                    "limit": 10,
3118                    "count": 1
3119                }"#,
3120            )
3121            .create_async()
3122            .await;
3123
3124        // create_rfq_quote
3125        let create_quote = RfqCreateQuote {
3126            request_id: "req123".to_string(),
3127            asset_in: "some_asset_in".to_string(),
3128            asset_out: "some_asset_out".to_string(),
3129            amount_in: "100".to_string(),
3130            amount_out: "200".to_string(),
3131            user_type: 0,
3132        };
3133        let create_quote_mock = server
3134            .mock("POST", "/rfq/quote")
3135            .match_body(Matcher::JsonString(
3136                json!({
3137                    "requestId": "req123",
3138                    "assetIn": "some_asset_in",
3139                    "assetOut": "some_asset_out",
3140                    "amountIn": "100",
3141                    "amountOut": "200",
3142                    "userType": 0
3143                })
3144                .to_string(),
3145            ))
3146            .with_status(200)
3147            .with_header("content-type", "application/json")
3148            .with_body(r#"{"quoteId":"q123"}"#)
3149            .create_async()
3150            .await;
3151
3152        // cancel_rfq_quote
3153        let cancel_quote_mock = server
3154            .mock("DELETE", "/rfq/quote")
3155            .match_body(Matcher::JsonString(r#"{"quoteId":"q123"}"#.to_string()))
3156            .with_status(200)
3157            .with_body("OK")
3158            .create_async()
3159            .await;
3160
3161        // get_rfq_requester_quotes
3162        let requester_quotes_mock = server
3163            .mock("GET", "/rfq/data/requester/quotes")
3164            .match_query(Matcher::AllOf(vec![
3165                Matcher::UrlEncoded("offset".into(), "MA==".into()),
3166                Matcher::UrlEncoded("limit".into(), "10".into()),
3167                Matcher::UrlEncoded("state".into(), "active".into()),
3168                Matcher::UrlEncoded("quoteIds[]".into(), "q123".into()),
3169                Matcher::UrlEncoded("requestIds[]".into(), "req123".into()),
3170            ]))
3171            .with_status(200)
3172            .with_header("content-type", "application/json")
3173            .with_body(
3174                r#"{
3175                    "data": [{
3176                        "quoteId": "q123",
3177                        "requestId": "req123",
3178                        "userAddress": "0xabc",
3179                        "proxyAddress": "0xdef",
3180                        "condition": "some_condition_id",
3181                        "token": "some_token_id",
3182                        "complement": "some_complement",
3183                        "side": "BUY",
3184                        "sizeIn": 100,
3185                        "sizeOut": 200,
3186                        "price": 0.5,
3187                        "matchType": "matched",
3188                        "state": "active"
3189                    }],
3190                    "next_cursor": "MA==",
3191                    "limit": 10,
3192                    "count": 1
3193                }"#,
3194            )
3195            .create_async()
3196            .await;
3197
3198        // get_rfq_quoter_quotes
3199        let quoter_quotes_mock = server
3200            .mock("GET", "/rfq/data/quoter/quotes")
3201            .with_status(200)
3202            .with_header("content-type", "application/json")
3203            .with_body(
3204                r#"{
3205                    "data": [],
3206                    "next_cursor": "MA==",
3207                    "limit": 10,
3208                    "count": 0
3209                }"#,
3210            )
3211            .create_async()
3212            .await;
3213
3214        // get_rfq_best_quote
3215        let best_quote_mock = server
3216            .mock("GET", "/rfq/data/best-quote")
3217            .match_query(Matcher::UrlEncoded("requestId".into(), "req123".into()))
3218            .with_status(200)
3219            .with_header("content-type", "application/json")
3220            .with_body(
3221                r#"{
3222                    "quoteId": "q123",
3223                    "requestId": "req123",
3224                    "userAddress": "0xabc",
3225                    "proxyAddress": "0xdef",
3226                    "condition": "some_condition_id",
3227                    "token": "some_token_id",
3228                    "complement": "some_complement",
3229                    "side": "BUY",
3230                    "sizeIn": 100,
3231                    "sizeOut": 200,
3232                    "price": 0.5,
3233                    "matchType": "matched",
3234                    "state": "active"
3235                }"#,
3236            )
3237            .create_async()
3238            .await;
3239
3240        // accept_rfq_quote
3241        let exec = RfqOrderExecutionRequest {
3242            request_id: "req123".to_string(),
3243            quote_id: "q123".to_string(),
3244            maker: "0xmaker".to_string(),
3245            signer: "0xsigner".to_string(),
3246            taker: "0xtaker".to_string(),
3247            expiration: 1_740_000_000,
3248            nonce: "123".to_string(),
3249            fee_rate_bps: "1000".to_string(),
3250            side: "BUY".to_string(),
3251            token_id: "123".to_string(),
3252            maker_amount: "100".to_string(),
3253            taker_amount: "200".to_string(),
3254            signature_type: 2,
3255            signature: "0xsig".to_string(),
3256            salt: 42,
3257            owner: "owner".to_string(),
3258        };
3259
3260        let accept_mock = server
3261            .mock("POST", "/rfq/request/accept")
3262            .match_body(Matcher::JsonString(
3263                json!({
3264                    "requestId": "req123",
3265                    "quoteId": "q123",
3266                    "maker": "0xmaker",
3267                    "signer": "0xsigner",
3268                    "taker": "0xtaker",
3269                    "expiration": 1740000000,
3270                    "nonce": "123",
3271                    "feeRateBps": "1000",
3272                    "side": "BUY",
3273                    "tokenId": "123",
3274                    "makerAmount": "100",
3275                    "takerAmount": "200",
3276                    "signatureType": 2,
3277                    "signature": "0xsig",
3278                    "salt": 42,
3279                    "owner": "owner"
3280                })
3281                .to_string(),
3282            ))
3283            .with_status(200)
3284            .with_body("OK")
3285            .create_async()
3286            .await;
3287
3288        // approve_rfq_order
3289        let approve_mock = server
3290            .mock("POST", "/rfq/quote/approve")
3291            .match_body(Matcher::JsonString(
3292                json!({
3293                    "requestId": "req123",
3294                    "quoteId": "q123",
3295                    "maker": "0xmaker",
3296                    "signer": "0xsigner",
3297                    "taker": "0xtaker",
3298                    "expiration": 1740000000,
3299                    "nonce": "123",
3300                    "feeRateBps": "1000",
3301                    "side": "BUY",
3302                    "tokenId": "123",
3303                    "makerAmount": "100",
3304                    "takerAmount": "200",
3305                    "signatureType": 2,
3306                    "signature": "0xsig",
3307                    "salt": 42,
3308                    "owner": "owner"
3309                })
3310                .to_string(),
3311            ))
3312            .with_status(200)
3313            .with_header("content-type", "application/json")
3314            .with_body(r#"{"tradeIds":["t1","t2"]}"#)
3315            .create_async()
3316            .await;
3317
3318        let client = create_test_client_with_l2_auth(&server.url());
3319
3320        let created = client.create_rfq_request(&create_request).await.unwrap();
3321        assert_eq!(created.request_id, "req123");
3322        assert_eq!(created.expiry, 1_744_936_318);
3323        create_request_mock.assert_async().await;
3324
3325        client.cancel_rfq_request("req123").await.unwrap();
3326        cancel_request_mock.assert_async().await;
3327
3328        let params = RfqRequestsParams {
3329            offset: Some("MA==".to_string()),
3330            limit: Some(10),
3331            state: Some("active".to_string()),
3332            request_ids: vec!["req123".to_string()],
3333            markets: vec!["some_market".to_string()],
3334            ..Default::default()
3335        };
3336        let requests = client.get_rfq_requests(Some(&params)).await.unwrap();
3337        assert_eq!(requests.data.len(), 1);
3338        assert_eq!(requests.data[0].request_id, "req123");
3339        rfq_requests_mock.assert_async().await;
3340
3341        let quote = client.create_rfq_quote(&create_quote).await.unwrap();
3342        assert_eq!(quote.quote_id, "q123");
3343        create_quote_mock.assert_async().await;
3344
3345        client.cancel_rfq_quote("q123").await.unwrap();
3346        cancel_quote_mock.assert_async().await;
3347
3348        let quote_params = RfqQuotesParams {
3349            offset: Some("MA==".to_string()),
3350            limit: Some(10),
3351            state: Some("active".to_string()),
3352            quote_ids: vec!["q123".to_string()],
3353            request_ids: vec!["req123".to_string()],
3354            ..Default::default()
3355        };
3356
3357        let requester_quotes = client
3358            .get_rfq_requester_quotes(Some(&quote_params))
3359            .await
3360            .unwrap();
3361        assert_eq!(requester_quotes.data.len(), 1);
3362        requester_quotes_mock.assert_async().await;
3363
3364        let quoter_quotes = client.get_rfq_quoter_quotes(None).await.unwrap();
3365        assert_eq!(quoter_quotes.data.len(), 0);
3366        quoter_quotes_mock.assert_async().await;
3367
3368        let best = client.get_rfq_best_quote("req123").await.unwrap();
3369        assert_eq!(best.quote_id, "q123");
3370        best_quote_mock.assert_async().await;
3371
3372        client.accept_rfq_quote(&exec).await.unwrap();
3373        accept_mock.assert_async().await;
3374
3375        let approved = client.approve_rfq_order(&exec).await.unwrap();
3376        assert_eq!(approved.trade_ids, vec!["t1".to_string(), "t2".to_string()]);
3377        approve_mock.assert_async().await;
3378    }
3379}