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