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