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