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