1use std::collections::HashMap;
2
3use polyoxide_core::{HttpClient, QueryBuilder};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6
7use crate::{
8 error::ClobError,
9 request::{AuthMode, Request},
10 types::OrderSide,
11};
12
13#[derive(Clone)]
15pub struct Markets {
16 pub(crate) http_client: HttpClient,
17 pub(crate) chain_id: u64,
18}
19
20impl Markets {
21 pub fn get(&self, condition_id: impl Into<String>) -> Request<Market> {
23 Request::get(
24 self.http_client.clone(),
25 format!("/markets/{}", urlencoding::encode(&condition_id.into())),
26 AuthMode::None,
27 self.chain_id,
28 )
29 }
30
31 pub fn get_by_token_ids(
32 &self,
33 token_ids: impl Into<Vec<String>>,
34 ) -> Request<ListMarketsResponse> {
35 Request::get(
36 self.http_client.clone(),
37 "/markets",
38 AuthMode::None,
39 self.chain_id,
40 )
41 .query_many("clob_token_ids", token_ids.into())
42 }
43
44 pub fn list(&self) -> Request<ListMarketsResponse> {
46 Request::get(
47 self.http_client.clone(),
48 "/markets",
49 AuthMode::None,
50 self.chain_id,
51 )
52 }
53
54 pub fn order_book(&self, token_id: impl Into<String>) -> Request<OrderBook> {
56 Request::get(
57 self.http_client.clone(),
58 "/book",
59 AuthMode::None,
60 self.chain_id,
61 )
62 .query("token_id", token_id.into())
63 }
64
65 pub fn price(&self, token_id: impl Into<String>, side: OrderSide) -> Request<PriceResponse> {
67 Request::get(
68 self.http_client.clone(),
69 "/price",
70 AuthMode::None,
71 self.chain_id,
72 )
73 .query("token_id", token_id.into())
74 .query("side", side.as_str())
75 }
76
77 pub fn midpoint(&self, token_id: impl Into<String>) -> Request<MidpointResponse> {
79 Request::get(
80 self.http_client.clone(),
81 "/midpoint",
82 AuthMode::None,
83 self.chain_id,
84 )
85 .query("token_id", token_id.into())
86 }
87
88 pub fn prices_history(&self, token_id: impl Into<String>) -> Request<PricesHistoryResponse> {
90 self.prices_history_with(token_id, &PricesHistoryQuery::default())
91 }
92
93 pub fn prices_history_with(
95 &self,
96 token_id: impl Into<String>,
97 params: &PricesHistoryQuery,
98 ) -> Request<PricesHistoryResponse> {
99 Request::get(
100 self.http_client.clone(),
101 "/prices-history",
102 AuthMode::None,
103 self.chain_id,
104 )
105 .query("market", token_id.into())
106 .query_opt("interval", params.interval.as_deref())
107 .query_opt("fidelity", params.fidelity)
108 .query_opt("startTs", params.start_ts)
109 .query_opt("endTs", params.end_ts)
110 }
111
112 pub fn neg_risk(&self, token_id: impl Into<String>) -> Request<NegRiskResponse> {
114 Request::get(
115 self.http_client.clone(),
116 "/neg-risk".to_string(),
117 AuthMode::None,
118 self.chain_id,
119 )
120 .query("token_id", token_id.into())
121 }
122
123 pub fn fee_rate(&self, token_id: impl Into<String>) -> Request<FeeRateResponse> {
125 Request::get(
126 self.http_client.clone(),
127 "/fee-rate",
128 AuthMode::None,
129 self.chain_id,
130 )
131 .query("token_id", token_id.into())
132 }
133
134 pub fn tick_size(&self, token_id: impl Into<String>) -> Request<TickSizeResponse> {
136 Request::get(
137 self.http_client.clone(),
138 "/tick-size".to_string(),
139 AuthMode::None,
140 self.chain_id,
141 )
142 .query("token_id", token_id.into())
143 }
144
145 pub fn neg_risk_path(&self, token_id: impl Into<String>) -> Request<NegRiskResponse> {
147 Request::get(
148 self.http_client.clone(),
149 format!("/neg-risk/{}", urlencoding::encode(&token_id.into())),
150 AuthMode::None,
151 self.chain_id,
152 )
153 }
154
155 pub fn fee_rate_path(&self, token_id: impl Into<String>) -> Request<FeeRateResponse> {
157 Request::get(
158 self.http_client.clone(),
159 format!("/fee-rate/{}", urlencoding::encode(&token_id.into())),
160 AuthMode::None,
161 self.chain_id,
162 )
163 }
164
165 pub fn tick_size_path(&self, token_id: impl Into<String>) -> Request<TickSizeResponse> {
167 Request::get(
168 self.http_client.clone(),
169 format!("/tick-size/{}", urlencoding::encode(&token_id.into())),
170 AuthMode::None,
171 self.chain_id,
172 )
173 }
174
175 pub fn clob_market_details(
180 &self,
181 condition_id: impl Into<String>,
182 ) -> Request<ClobMarketDetails> {
183 Request::get(
184 self.http_client.clone(),
185 format!(
186 "/clob-markets/{}",
187 urlencoding::encode(&condition_id.into())
188 ),
189 AuthMode::None,
190 self.chain_id,
191 )
192 }
193
194 pub fn market_by_token(&self, token_id: impl Into<String>) -> Request<MarketByTokenResponse> {
199 Request::get(
200 self.http_client.clone(),
201 format!(
202 "/markets-by-token/{}",
203 urlencoding::encode(&token_id.into())
204 ),
205 AuthMode::None,
206 self.chain_id,
207 )
208 }
209
210 pub fn live_activity_market(
213 &self,
214 condition_id: impl Into<String>,
215 ) -> Request<LiveActivityMarket> {
216 Request::get(
217 self.http_client.clone(),
218 format!(
219 "/markets/live-activity/{}",
220 urlencoding::encode(&condition_id.into())
221 ),
222 AuthMode::None,
223 self.chain_id,
224 )
225 }
226
227 pub fn live_activity_bulk(
230 &self,
231 condition_ids: Vec<String>,
232 ) -> Result<Request<Vec<LiveActivityMarket>>, ClobError> {
233 Request::<Vec<LiveActivityMarket>>::post(
234 self.http_client.clone(),
235 "/markets/live-activity".to_string(),
236 AuthMode::None,
237 self.chain_id,
238 )
239 .body(&condition_ids)
240 }
241
242 pub fn batch_prices_history(
245 &self,
246 req: &BatchPricesHistoryRequest,
247 ) -> Result<Request<BatchPricesHistoryResponse>, ClobError> {
248 Request::<BatchPricesHistoryResponse>::post(
249 self.http_client.clone(),
250 "/batch-prices-history".to_string(),
251 AuthMode::None,
252 self.chain_id,
253 )
254 .body(req)
255 }
256
257 pub fn spread(&self, token_id: impl Into<String>) -> Request<SpreadResponse> {
259 Request::get(
260 self.http_client.clone(),
261 "/spread",
262 AuthMode::None,
263 self.chain_id,
264 )
265 .query("token_id", token_id.into())
266 }
267
268 pub fn last_trade_price(&self, token_id: impl Into<String>) -> Request<LastTradePriceResponse> {
270 Request::get(
271 self.http_client.clone(),
272 "/last-trade-price",
273 AuthMode::None,
274 self.chain_id,
275 )
276 .query("token_id", token_id.into())
277 }
278
279 pub fn simplified(&self) -> Request<ListMarketsResponse> {
281 Request::get(
282 self.http_client.clone(),
283 "/simplified-markets",
284 AuthMode::None,
285 self.chain_id,
286 )
287 }
288
289 pub fn sampling(&self) -> Request<ListMarketsResponse> {
291 Request::get(
292 self.http_client.clone(),
293 "/sampling-markets",
294 AuthMode::None,
295 self.chain_id,
296 )
297 }
298
299 pub fn sampling_simplified(&self) -> Request<ListMarketsResponse> {
301 Request::get(
302 self.http_client.clone(),
303 "/sampling-simplified-markets",
304 AuthMode::None,
305 self.chain_id,
306 )
307 }
308
309 pub async fn calculate_price(
311 &self,
312 token_id: impl Into<String>,
313 side: OrderSide,
314 amount: impl Into<String>,
315 ) -> Result<CalculatePriceResponse, ClobError> {
316 Request::<CalculatePriceResponse>::post(
317 self.http_client.clone(),
318 "/calculate-price".to_string(),
319 AuthMode::None,
320 self.chain_id,
321 )
322 .body(&CalculatePriceParams {
323 token_id: token_id.into(),
324 side,
325 amount: amount.into(),
326 })?
327 .send()
328 .await
329 }
330
331 pub async fn order_books(&self, params: &[BookParams]) -> Result<Vec<OrderBook>, ClobError> {
333 Request::<Vec<OrderBook>>::post(
334 self.http_client.clone(),
335 "/books".to_string(),
336 AuthMode::None,
337 self.chain_id,
338 )
339 .body(params)?
340 .send()
341 .await
342 }
343
344 pub async fn prices(&self, params: &[BookParams]) -> Result<Vec<PriceResponse>, ClobError> {
346 Request::<Vec<PriceResponse>>::post(
347 self.http_client.clone(),
348 "/prices".to_string(),
349 AuthMode::None,
350 self.chain_id,
351 )
352 .body(params)?
353 .send()
354 .await
355 }
356
357 pub async fn midpoints(
359 &self,
360 params: &[BookParams],
361 ) -> Result<Vec<MidpointResponse>, ClobError> {
362 Request::<Vec<MidpointResponse>>::post(
363 self.http_client.clone(),
364 "/midpoints".to_string(),
365 AuthMode::None,
366 self.chain_id,
367 )
368 .body(params)?
369 .send()
370 .await
371 }
372
373 pub async fn spreads(&self, params: &[BookParams]) -> Result<Vec<SpreadResponse>, ClobError> {
375 Request::<Vec<SpreadResponse>>::post(
376 self.http_client.clone(),
377 "/spreads".to_string(),
378 AuthMode::None,
379 self.chain_id,
380 )
381 .body(params)?
382 .send()
383 .await
384 }
385
386 pub async fn last_trade_prices(
388 &self,
389 params: &[BookParams],
390 ) -> Result<Vec<LastTradePriceResponse>, ClobError> {
391 Request::<Vec<LastTradePriceResponse>>::post(
392 self.http_client.clone(),
393 "/last-trades-prices".to_string(),
394 AuthMode::None,
395 self.chain_id,
396 )
397 .body(params)?
398 .send()
399 .await
400 }
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
405pub struct Market {
406 pub condition_id: String,
407 pub question_id: Option<String>,
408 pub tokens: Vec<MarketToken>,
409 pub rewards: Option<serde_json::Value>,
410 pub minimum_order_size: Option<f64>,
411 pub minimum_tick_size: Option<f64>,
412 pub description: Option<String>,
413 pub category: Option<String>,
414 pub end_date_iso: Option<String>,
415 pub question: Option<String>,
416 pub active: bool,
417 pub closed: bool,
418 pub archived: bool,
419 pub accepting_orders: Option<bool>,
420 pub neg_risk: Option<bool>,
421 pub neg_risk_market_id: Option<String>,
422 pub enable_order_book: Option<bool>,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct ListMarketsResponse {
428 pub data: Vec<Market>,
429 pub next_cursor: Option<String>,
430}
431
432#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct MarketToken {
435 pub token_id: Option<String>,
436 pub outcome: String,
437 pub price: Option<f64>,
438 pub winner: Option<bool>,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct OrderLevel {
444 #[serde(with = "rust_decimal::serde::str")]
445 pub price: Decimal,
446 #[serde(with = "rust_decimal::serde::str")]
447 pub size: Decimal,
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct OrderBook {
453 pub market: String,
454 pub asset_id: String,
455 pub bids: Vec<OrderLevel>,
456 pub asks: Vec<OrderLevel>,
457 pub timestamp: String,
458 pub hash: String,
459 pub min_order_size: Option<String>,
460 pub tick_size: Option<String>,
461 #[serde(default)]
462 pub neg_risk: Option<bool>,
463 pub last_trade_price: Option<String>,
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize)]
468pub struct PriceResponse {
469 pub price: String,
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize)]
474pub struct MidpointResponse {
475 pub mid: String,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize)]
480pub struct PriceHistoryPoint {
481 #[serde(rename = "t")]
483 pub timestamp: i64,
484 #[serde(rename = "p")]
486 pub price: f64,
487}
488
489#[derive(Debug, Clone, Default)]
495pub struct PricesHistoryQuery {
496 pub interval: Option<String>,
498 pub fidelity: Option<i32>,
500 pub start_ts: Option<i64>,
502 pub end_ts: Option<i64>,
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct PricesHistoryResponse {
509 pub history: Vec<PriceHistoryPoint>,
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize)]
514pub struct NegRiskResponse {
515 pub neg_risk: bool,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize)]
520pub struct FeeRateResponse {
521 pub base_fee: u32,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct TickSizeResponse {
527 #[serde(deserialize_with = "deserialize_tick_size")]
528 pub minimum_tick_size: String,
529}
530
531#[derive(Debug, Clone, Serialize)]
533pub struct BookParams {
534 pub token_id: String,
535 #[serde(skip_serializing_if = "Option::is_none")]
536 pub side: Option<OrderSide>,
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize)]
541pub struct SpreadResponse {
542 pub token_id: Option<String>,
543 pub spread: String,
544 pub bid: Option<String>,
545 pub ask: Option<String>,
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct LastTradePriceResponse {
551 pub token_id: Option<String>,
552 pub price: Option<String>,
553 pub last_trade_price: Option<String>,
554 pub side: Option<String>,
555 pub timestamp: Option<String>,
556}
557
558#[derive(Debug, Clone, Serialize)]
560pub struct CalculatePriceParams {
561 pub token_id: String,
562 pub side: OrderSide,
563 pub amount: String,
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize)]
568pub struct CalculatePriceResponse {
569 pub price: String,
570}
571
572fn deserialize_tick_size<'de, D>(deserializer: D) -> Result<String, D::Error>
573where
574 D: serde::Deserializer<'de>,
575{
576 use serde::Deserialize;
577 let v = serde_json::Value::deserialize(deserializer)?;
578 match v {
579 serde_json::Value::String(s) => Ok(s),
580 serde_json::Value::Number(n) => Ok(n.to_string()),
581 _ => Err(serde::de::Error::custom(
582 "expected string or number for tick size",
583 )),
584 }
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct ClobToken {
593 pub t: String,
595 pub o: String,
597}
598
599#[derive(Debug, Clone, Serialize, Deserialize)]
604pub struct FeeDetails {
605 pub r: Option<f64>,
607 pub e: Option<f64>,
609 pub to: Option<bool>,
611}
612
613#[derive(Debug, Clone, Default, Serialize, Deserialize)]
618#[serde(transparent)]
619pub struct ClobRewards {
620 pub extra: HashMap<String, serde_json::Value>,
622}
623
624#[derive(Debug, Clone, Serialize, Deserialize)]
634pub struct ClobMarketDetails {
635 pub gst: Option<String>,
637 pub r: ClobRewards,
639 pub t: Vec<ClobToken>,
641 pub mos: f64,
643 pub mts: f64,
645 pub mbf: Option<i64>,
647 pub tbf: Option<i64>,
649 pub rfqe: Option<bool>,
651 pub itode: Option<bool>,
653 pub ibce: bool,
655 pub fd: Option<FeeDetails>,
657 pub oas: Option<i32>,
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize)]
664pub struct MarketByTokenResponse {
665 pub condition_id: String,
667 pub primary_token_id: String,
669 pub secondary_token_id: String,
671}
672
673#[derive(Debug, Clone, Serialize, Deserialize)]
676pub struct LiveActivityMarket {
677 pub condition_id: Option<String>,
679 pub id: Option<i64>,
681 pub question: Option<String>,
683 pub market_slug: Option<String>,
685 pub event_slug: Option<String>,
687 pub series_slug: Option<String>,
689 pub icon: Option<String>,
691 pub image: Option<String>,
693 #[serde(default)]
695 pub tags: Vec<String>,
696}
697
698#[derive(Debug, Clone, Serialize, Deserialize)]
703pub struct MarketPrice {
704 pub t: u32,
706 pub p: f64,
708}
709
710#[derive(Debug, Clone, Default, Serialize, Deserialize)]
712pub struct BatchPricesHistoryRequest {
713 pub markets: Vec<String>,
715 #[serde(skip_serializing_if = "Option::is_none")]
717 pub start_ts: Option<f64>,
718 #[serde(skip_serializing_if = "Option::is_none")]
720 pub end_ts: Option<f64>,
721 #[serde(skip_serializing_if = "Option::is_none")]
723 pub interval: Option<String>,
724 #[serde(skip_serializing_if = "Option::is_none")]
726 pub fidelity: Option<i32>,
727}
728
729#[derive(Debug, Clone, Default, Serialize, Deserialize)]
732pub struct BatchPricesHistoryResponse {
733 pub history: HashMap<String, Vec<MarketPrice>>,
735}
736
737#[cfg(test)]
738mod tests {
739 use super::*;
740
741 #[test]
742 fn test_fee_rate_response_deserializes() {
743 let json = r#"{"base_fee": 100}"#;
744 let resp: FeeRateResponse = serde_json::from_str(json).unwrap();
745 assert_eq!(resp.base_fee, 100);
746 }
747
748 #[test]
749 fn test_fee_rate_response_deserializes_zero() {
750 let json = r#"{"base_fee": 0}"#;
751 let resp: FeeRateResponse = serde_json::from_str(json).unwrap();
752 assert_eq!(resp.base_fee, 0);
753 }
754
755 #[test]
756 fn test_fee_rate_response_rejects_missing_field() {
757 let json = r#"{"feeRate": "100"}"#;
758 let result = serde_json::from_str::<FeeRateResponse>(json);
759 assert!(result.is_err(), "Should reject JSON missing base_fee field");
760 }
761
762 #[test]
763 fn test_fee_rate_response_rejects_empty_json() {
764 let json = r#"{}"#;
765 let result = serde_json::from_str::<FeeRateResponse>(json);
766 assert!(result.is_err(), "Should reject empty JSON object");
767 }
768
769 #[test]
770 fn book_params_serializes() {
771 let params = BookParams {
772 token_id: "token-1".into(),
773 side: Some(OrderSide::Buy),
774 };
775 let json = serde_json::to_value(¶ms).unwrap();
776 assert_eq!(json["token_id"], "token-1");
777 assert_eq!(json["side"], "BUY");
778 }
779
780 #[test]
781 fn book_params_omits_none_side() {
782 let params = BookParams {
783 token_id: "token-1".into(),
784 side: None,
785 };
786 let json = serde_json::to_value(¶ms).unwrap();
787 assert_eq!(json["token_id"], "token-1");
788 assert!(json.get("side").is_none());
789 }
790
791 #[test]
792 fn spread_response_deserializes() {
793 let json = r#"{
794 "token_id": "token-1",
795 "spread": "0.02",
796 "bid": "0.48",
797 "ask": "0.50"
798 }"#;
799 let resp: SpreadResponse = serde_json::from_str(json).unwrap();
800 assert_eq!(resp.token_id.as_deref(), Some("token-1"));
801 assert_eq!(resp.spread, "0.02");
802 assert_eq!(resp.bid.as_deref(), Some("0.48"));
803 assert_eq!(resp.ask.as_deref(), Some("0.50"));
804 }
805
806 #[test]
807 fn last_trade_price_response_deserializes() {
808 let json = r#"{
809 "token_id": "token-1",
810 "last_trade_price": "0.55",
811 "timestamp": "1700000000"
812 }"#;
813 let resp: LastTradePriceResponse = serde_json::from_str(json).unwrap();
814 assert_eq!(resp.token_id.as_deref(), Some("token-1"));
815 assert_eq!(resp.last_trade_price.as_deref(), Some("0.55"));
816 assert_eq!(resp.timestamp.as_deref(), Some("1700000000"));
817 }
818
819 #[test]
820 fn calculate_price_params_serializes() {
821 let params = CalculatePriceParams {
822 token_id: "token-1".into(),
823 side: OrderSide::Buy,
824 amount: "100.0".into(),
825 };
826 let json = serde_json::to_value(¶ms).unwrap();
827 assert_eq!(json["token_id"], "token-1");
828 assert_eq!(json["side"], "BUY");
829 assert_eq!(json["amount"], "100.0");
830 }
831
832 #[test]
833 fn calculate_price_response_deserializes() {
834 let json = r#"{"price": "0.52"}"#;
835 let resp: CalculatePriceResponse = serde_json::from_str(json).unwrap();
836 assert_eq!(resp.price, "0.52");
837 }
838
839 #[test]
840 fn order_book_deserializes_with_new_fields() {
841 let json = r#"{
842 "market": "0xcond",
843 "asset_id": "0xtoken",
844 "bids": [{"price": "0.48", "size": "100"}],
845 "asks": [{"price": "0.52", "size": "200"}],
846 "timestamp": "1700000000",
847 "hash": "abc123",
848 "min_order_size": "5",
849 "tick_size": "0.001",
850 "neg_risk": false,
851 "last_trade_price": "0.50"
852 }"#;
853 let ob: OrderBook = serde_json::from_str(json).unwrap();
854 assert_eq!(ob.market, "0xcond");
855 assert_eq!(ob.bids.len(), 1);
856 assert_eq!(ob.asks.len(), 1);
857 assert_eq!(ob.min_order_size.as_deref(), Some("5"));
858 assert_eq!(ob.tick_size.as_deref(), Some("0.001"));
859 assert_eq!(ob.neg_risk, Some(false));
860 assert_eq!(ob.last_trade_price.as_deref(), Some("0.50"));
861 }
862
863 #[test]
864 fn order_book_deserializes_without_new_fields() {
865 let json = r#"{
866 "market": "0xcond",
867 "asset_id": "0xtoken",
868 "bids": [],
869 "asks": [],
870 "timestamp": "1700000000",
871 "hash": "abc123"
872 }"#;
873 let ob: OrderBook = serde_json::from_str(json).unwrap();
874 assert_eq!(ob.market, "0xcond");
875 assert!(ob.min_order_size.is_none());
876 assert!(ob.tick_size.is_none());
877 assert!(ob.neg_risk.is_none());
878 assert!(ob.last_trade_price.is_none());
879 }
880
881 #[test]
882 fn clob_market_details_roundtrip() {
883 let json = r#"{
885 "gst": null,
886 "r": {"minSize": 100, "maxSpread": 2.0},
887 "t": [
888 {"t": "71321045679252212594626385532706912750332728571942532289631379312455583992563", "o": "Yes"},
889 {"t": "52114319501245915516055106046884209969926127482827954674443846427813813222426", "o": "No"}
890 ],
891 "mos": 5.0,
892 "mts": 0.01,
893 "mbf": 0,
894 "tbf": 0,
895 "rfqe": true,
896 "itode": false,
897 "ibce": true,
898 "fd": {"r": 0.02, "e": 2.0, "to": true},
899 "oas": 0
900 }"#;
901 let parsed: ClobMarketDetails = serde_json::from_str(json).unwrap();
902 assert!(parsed.gst.is_none());
903 assert_eq!(parsed.t.len(), 2);
904 assert_eq!(parsed.t[0].o, "Yes");
905 assert!((parsed.mos - 5.0).abs() < f64::EPSILON);
906 assert!((parsed.mts - 0.01).abs() < f64::EPSILON);
907 assert_eq!(parsed.mbf, Some(0));
908 assert_eq!(parsed.tbf, Some(0));
909 assert_eq!(parsed.rfqe, Some(true));
910 assert_eq!(parsed.itode, Some(false));
911 assert!(parsed.ibce);
912 let fd = parsed.fd.as_ref().expect("fd present in full payload");
913 assert_eq!(fd.r, Some(0.02));
914 assert_eq!(fd.e, Some(2.0));
915 assert_eq!(fd.to, Some(true));
916 assert_eq!(parsed.oas, Some(0));
917 assert!(parsed.r.extra.contains_key("minSize"));
919
920 let back = serde_json::to_value(&parsed).unwrap();
922 let again: ClobMarketDetails = serde_json::from_value(back).unwrap();
923 assert_eq!(again.t.len(), 2);
924 assert_eq!(again.fd.as_ref().unwrap().r, Some(0.02));
925 }
926
927 #[test]
928 fn clob_market_details_resolved_market_deserializes() {
929 let json = r#"{
934 "c": false,
935 "cbos": false,
936 "gst": null,
937 "ibce": true,
938 "mos": 5.0,
939 "mts": 0.01,
940 "r": {},
941 "sd": false,
942 "t": [
943 {"t": "713210456", "o": "Yes"},
944 {"t": "521143195", "o": "No"}
945 ]
946 }"#;
947 let parsed: ClobMarketDetails = serde_json::from_str(json)
948 .expect("resolved market (reduced payload) should deserialize");
949 assert_eq!(parsed.t.len(), 2);
950 assert!(parsed.ibce);
951 assert!(parsed.mbf.is_none());
952 assert!(parsed.tbf.is_none());
953 assert!(parsed.rfqe.is_none());
954 assert!(parsed.itode.is_none());
955 assert!(parsed.fd.is_none());
956 assert!(parsed.oas.is_none());
957 }
958
959 #[test]
960 fn market_by_token_response_deserializes() {
961 let json = r#"{
962 "condition_id": "0xbd31dc8a",
963 "primary_token_id": "713210456",
964 "secondary_token_id": "521143195"
965 }"#;
966 let parsed: MarketByTokenResponse = serde_json::from_str(json).unwrap();
967 assert_eq!(parsed.condition_id, "0xbd31dc8a");
968 assert_eq!(parsed.primary_token_id, "713210456");
969 assert_eq!(parsed.secondary_token_id, "521143195");
970 }
971
972 #[test]
973 fn live_activity_market_roundtrip() {
974 let json = r#"{
975 "condition_id": "0xcond",
976 "id": 42,
977 "question": "Will X happen?",
978 "market_slug": "will-x-happen",
979 "event_slug": "x-event",
980 "series_slug": null,
981 "icon": "https://icon",
982 "image": "https://image",
983 "tags": ["crypto", "sports"]
984 }"#;
985 let parsed: LiveActivityMarket = serde_json::from_str(json).unwrap();
986 assert_eq!(parsed.condition_id.as_deref(), Some("0xcond"));
987 assert_eq!(parsed.id, Some(42));
988 assert_eq!(parsed.question.as_deref(), Some("Will X happen?"));
989 assert_eq!(parsed.market_slug.as_deref(), Some("will-x-happen"));
990 assert_eq!(parsed.event_slug.as_deref(), Some("x-event"));
991 assert!(parsed.series_slug.is_none());
992 assert_eq!(parsed.tags, vec!["crypto", "sports"]);
993
994 let back: LiveActivityMarket =
996 serde_json::from_value(serde_json::to_value(&parsed).unwrap()).unwrap();
997 assert_eq!(back.id, Some(42));
998 }
999
1000 #[test]
1001 fn batch_prices_history_request_omits_none_fields() {
1002 let req = BatchPricesHistoryRequest {
1003 markets: vec!["0xtoken1".into(), "0xtoken2".into()],
1004 start_ts: Some(1_700_000_000.0),
1005 end_ts: None,
1006 interval: Some("1d".into()),
1007 fidelity: None,
1008 };
1009 let json = serde_json::to_value(&req).unwrap();
1010 assert_eq!(json["markets"][0], "0xtoken1");
1011 assert!((json["start_ts"].as_f64().unwrap() - 1_700_000_000.0).abs() < f64::EPSILON);
1012 assert_eq!(json["interval"], "1d");
1013 assert!(json.get("end_ts").is_none());
1014 assert!(json.get("fidelity").is_none());
1015 }
1016
1017 #[test]
1018 fn batch_prices_history_response_roundtrip() {
1019 let json = r#"{
1020 "history": {
1021 "0xtokenA": [
1022 {"t": 1700000000, "p": 0.55},
1023 {"t": 1700001000, "p": 0.60}
1024 ],
1025 "0xtokenB": [
1026 {"t": 1700000000, "p": 0.30}
1027 ]
1028 }
1029 }"#;
1030 let parsed: BatchPricesHistoryResponse = serde_json::from_str(json).unwrap();
1031 assert_eq!(parsed.history.len(), 2);
1032 let a = parsed.history.get("0xtokenA").unwrap();
1033 assert_eq!(a.len(), 2);
1034 assert_eq!(a[0].t, 1_700_000_000);
1035 assert!((a[0].p - 0.55).abs() < f64::EPSILON);
1036 let b = parsed.history.get("0xtokenB").unwrap();
1037 assert_eq!(b.len(), 1);
1038 assert!((b[0].p - 0.30).abs() < f64::EPSILON);
1039
1040 let back: BatchPricesHistoryResponse =
1042 serde_json::from_value(serde_json::to_value(&parsed).unwrap()).unwrap();
1043 assert_eq!(back.history.len(), 2);
1044 }
1045}