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