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) -> ListClobMarkets {
50 ListClobMarkets {
51 request: Request::get(
52 self.http_client.clone(),
53 "/markets",
54 AuthMode::None,
55 self.chain_id,
56 ),
57 }
58 }
59
60 pub fn order_book(&self, token_id: impl Into<String>) -> Request<OrderBook> {
62 Request::get(
63 self.http_client.clone(),
64 "/book",
65 AuthMode::None,
66 self.chain_id,
67 )
68 .query("token_id", token_id.into())
69 }
70
71 pub fn price(&self, token_id: impl Into<String>, side: OrderSide) -> Request<PriceResponse> {
73 Request::get(
74 self.http_client.clone(),
75 "/price",
76 AuthMode::None,
77 self.chain_id,
78 )
79 .query("token_id", token_id.into())
80 .query("side", side.as_str())
81 }
82
83 pub fn midpoint(&self, token_id: impl Into<String>) -> Request<MidpointResponse> {
85 Request::get(
86 self.http_client.clone(),
87 "/midpoint",
88 AuthMode::None,
89 self.chain_id,
90 )
91 .query("token_id", token_id.into())
92 }
93
94 pub fn prices_history(&self, token_id: impl Into<String>) -> Request<PricesHistoryResponse> {
96 self.prices_history_with(token_id, &PricesHistoryQuery::default())
97 }
98
99 pub fn prices_history_with(
101 &self,
102 token_id: impl Into<String>,
103 params: &PricesHistoryQuery,
104 ) -> Request<PricesHistoryResponse> {
105 Request::get(
106 self.http_client.clone(),
107 "/prices-history",
108 AuthMode::None,
109 self.chain_id,
110 )
111 .query("market", token_id.into())
112 .query_opt("interval", params.interval.as_deref())
113 .query_opt("fidelity", params.fidelity)
114 .query_opt("startTs", params.start_ts)
115 .query_opt("endTs", params.end_ts)
116 }
117
118 pub fn neg_risk(&self, token_id: impl Into<String>) -> Request<NegRiskResponse> {
120 Request::get(
121 self.http_client.clone(),
122 "/neg-risk".to_string(),
123 AuthMode::None,
124 self.chain_id,
125 )
126 .query("token_id", token_id.into())
127 }
128
129 pub fn fee_rate(&self, token_id: impl Into<String>) -> Request<FeeRateResponse> {
131 Request::get(
132 self.http_client.clone(),
133 "/fee-rate",
134 AuthMode::None,
135 self.chain_id,
136 )
137 .query("token_id", token_id.into())
138 }
139
140 pub fn tick_size(&self, token_id: impl Into<String>) -> Request<TickSizeResponse> {
142 Request::get(
143 self.http_client.clone(),
144 "/tick-size".to_string(),
145 AuthMode::None,
146 self.chain_id,
147 )
148 .query("token_id", token_id.into())
149 }
150
151 pub fn neg_risk_path(&self, token_id: impl Into<String>) -> Request<NegRiskResponse> {
153 Request::get(
154 self.http_client.clone(),
155 format!("/neg-risk/{}", urlencoding::encode(&token_id.into())),
156 AuthMode::None,
157 self.chain_id,
158 )
159 }
160
161 pub fn fee_rate_path(&self, token_id: impl Into<String>) -> Request<FeeRateResponse> {
163 Request::get(
164 self.http_client.clone(),
165 format!("/fee-rate/{}", urlencoding::encode(&token_id.into())),
166 AuthMode::None,
167 self.chain_id,
168 )
169 }
170
171 pub fn tick_size_path(&self, token_id: impl Into<String>) -> Request<TickSizeResponse> {
173 Request::get(
174 self.http_client.clone(),
175 format!("/tick-size/{}", urlencoding::encode(&token_id.into())),
176 AuthMode::None,
177 self.chain_id,
178 )
179 }
180
181 pub fn clob_market_details(
186 &self,
187 condition_id: impl Into<String>,
188 ) -> Request<ClobMarketDetails> {
189 Request::get(
190 self.http_client.clone(),
191 format!(
192 "/clob-markets/{}",
193 urlencoding::encode(&condition_id.into())
194 ),
195 AuthMode::None,
196 self.chain_id,
197 )
198 }
199
200 pub fn market_by_token(&self, token_id: impl Into<String>) -> Request<MarketByTokenResponse> {
205 Request::get(
206 self.http_client.clone(),
207 format!(
208 "/markets-by-token/{}",
209 urlencoding::encode(&token_id.into())
210 ),
211 AuthMode::None,
212 self.chain_id,
213 )
214 }
215
216 pub fn live_activity_market(
219 &self,
220 condition_id: impl Into<String>,
221 ) -> Request<LiveActivityMarket> {
222 Request::get(
223 self.http_client.clone(),
224 format!(
225 "/markets/live-activity/{}",
226 urlencoding::encode(&condition_id.into())
227 ),
228 AuthMode::None,
229 self.chain_id,
230 )
231 }
232
233 pub fn live_activity_bulk(
236 &self,
237 condition_ids: Vec<String>,
238 ) -> Result<Request<Vec<LiveActivityMarket>>, ClobError> {
239 Request::<Vec<LiveActivityMarket>>::post(
240 self.http_client.clone(),
241 "/markets/live-activity".to_string(),
242 AuthMode::None,
243 self.chain_id,
244 )
245 .body(&condition_ids)
246 }
247
248 pub fn batch_prices_history(
251 &self,
252 req: &BatchPricesHistoryRequest,
253 ) -> Result<Request<BatchPricesHistoryResponse>, ClobError> {
254 Request::<BatchPricesHistoryResponse>::post(
255 self.http_client.clone(),
256 "/batch-prices-history".to_string(),
257 AuthMode::None,
258 self.chain_id,
259 )
260 .body(req)
261 }
262
263 pub fn spread(&self, token_id: impl Into<String>) -> Request<SpreadResponse> {
265 Request::get(
266 self.http_client.clone(),
267 "/spread",
268 AuthMode::None,
269 self.chain_id,
270 )
271 .query("token_id", token_id.into())
272 }
273
274 pub fn last_trade_price(&self, token_id: impl Into<String>) -> Request<LastTradePriceResponse> {
276 Request::get(
277 self.http_client.clone(),
278 "/last-trade-price",
279 AuthMode::None,
280 self.chain_id,
281 )
282 .query("token_id", token_id.into())
283 }
284
285 pub fn simplified(&self) -> ListClobMarkets {
289 ListClobMarkets {
290 request: Request::get(
291 self.http_client.clone(),
292 "/simplified-markets",
293 AuthMode::None,
294 self.chain_id,
295 ),
296 }
297 }
298
299 pub fn sampling(&self) -> ListClobMarkets {
303 ListClobMarkets {
304 request: Request::get(
305 self.http_client.clone(),
306 "/sampling-markets",
307 AuthMode::None,
308 self.chain_id,
309 ),
310 }
311 }
312
313 pub fn sampling_simplified(&self) -> ListClobMarkets {
317 ListClobMarkets {
318 request: Request::get(
319 self.http_client.clone(),
320 "/sampling-simplified-markets",
321 AuthMode::None,
322 self.chain_id,
323 ),
324 }
325 }
326
327 pub async fn calculate_price(
329 &self,
330 token_id: impl Into<String>,
331 side: OrderSide,
332 amount: impl Into<String>,
333 ) -> Result<CalculatePriceResponse, ClobError> {
334 Request::<CalculatePriceResponse>::post(
335 self.http_client.clone(),
336 "/calculate-price".to_string(),
337 AuthMode::None,
338 self.chain_id,
339 )
340 .body(&CalculatePriceParams {
341 token_id: token_id.into(),
342 side,
343 amount: amount.into(),
344 })?
345 .send()
346 .await
347 }
348
349 pub async fn order_books(&self, params: &[BookParams]) -> Result<Vec<OrderBook>, ClobError> {
351 Request::<Vec<OrderBook>>::post(
352 self.http_client.clone(),
353 "/books".to_string(),
354 AuthMode::None,
355 self.chain_id,
356 )
357 .body(params)?
358 .send()
359 .await
360 }
361
362 pub async fn prices(&self, params: &[BookParams]) -> Result<Vec<PriceResponse>, ClobError> {
364 Request::<Vec<PriceResponse>>::post(
365 self.http_client.clone(),
366 "/prices".to_string(),
367 AuthMode::None,
368 self.chain_id,
369 )
370 .body(params)?
371 .send()
372 .await
373 }
374
375 pub async fn midpoints(
377 &self,
378 params: &[BookParams],
379 ) -> Result<Vec<MidpointResponse>, ClobError> {
380 Request::<Vec<MidpointResponse>>::post(
381 self.http_client.clone(),
382 "/midpoints".to_string(),
383 AuthMode::None,
384 self.chain_id,
385 )
386 .body(params)?
387 .send()
388 .await
389 }
390
391 pub async fn spreads(&self, params: &[BookParams]) -> Result<Vec<SpreadResponse>, ClobError> {
393 Request::<Vec<SpreadResponse>>::post(
394 self.http_client.clone(),
395 "/spreads".to_string(),
396 AuthMode::None,
397 self.chain_id,
398 )
399 .body(params)?
400 .send()
401 .await
402 }
403
404 pub async fn last_trade_prices(
406 &self,
407 params: &[BookParams],
408 ) -> Result<Vec<LastTradePriceResponse>, ClobError> {
409 Request::<Vec<LastTradePriceResponse>>::post(
410 self.http_client.clone(),
411 "/last-trades-prices".to_string(),
412 AuthMode::None,
413 self.chain_id,
414 )
415 .body(params)?
416 .send()
417 .await
418 }
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct Market {
424 pub condition_id: String,
425 pub question_id: Option<String>,
426 pub tokens: Vec<MarketToken>,
427 pub rewards: Option<serde_json::Value>,
428 pub minimum_order_size: Option<f64>,
429 pub minimum_tick_size: Option<f64>,
430 pub description: Option<String>,
431 pub category: Option<String>,
432 pub end_date_iso: Option<String>,
433 pub question: Option<String>,
434 pub active: bool,
435 pub closed: bool,
436 pub archived: bool,
437 pub accepting_orders: Option<bool>,
438 pub neg_risk: Option<bool>,
439 pub neg_risk_market_id: Option<String>,
440 pub enable_order_book: Option<bool>,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct ListMarketsResponse {
446 pub data: Vec<Market>,
447 pub next_cursor: Option<String>,
448}
449
450pub struct ListClobMarkets {
454 request: Request<ListMarketsResponse>,
455}
456
457impl ListClobMarkets {
458 pub fn next_cursor(mut self, cursor: impl Into<String>) -> Self {
463 self.request = self.request.query("next_cursor", cursor.into());
464 self
465 }
466
467 pub async fn send(self) -> Result<ListMarketsResponse, ClobError> {
469 self.request.send().await
470 }
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize)]
475pub struct MarketToken {
476 pub token_id: Option<String>,
477 pub outcome: String,
478 pub price: Option<f64>,
479 pub winner: Option<bool>,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
484pub struct OrderLevel {
485 #[serde(with = "rust_decimal::serde::str")]
486 pub price: Decimal,
487 #[serde(with = "rust_decimal::serde::str")]
488 pub size: Decimal,
489}
490
491#[derive(Debug, Clone, Serialize, Deserialize)]
493pub struct OrderBook {
494 pub market: String,
495 pub asset_id: String,
496 pub bids: Vec<OrderLevel>,
497 pub asks: Vec<OrderLevel>,
498 pub timestamp: String,
499 pub hash: String,
500 pub min_order_size: Option<String>,
501 pub tick_size: Option<String>,
502 #[serde(default)]
503 pub neg_risk: Option<bool>,
504 pub last_trade_price: Option<String>,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
509pub struct PriceResponse {
510 pub price: String,
511}
512
513#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct MidpointResponse {
516 pub mid: String,
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize)]
521pub struct PriceHistoryPoint {
522 #[serde(rename = "t")]
524 pub timestamp: i64,
525 #[serde(rename = "p")]
527 pub price: f64,
528}
529
530#[derive(Debug, Clone, Default)]
536pub struct PricesHistoryQuery {
537 pub interval: Option<String>,
539 pub fidelity: Option<i32>,
541 pub start_ts: Option<i64>,
543 pub end_ts: Option<i64>,
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct PricesHistoryResponse {
550 pub history: Vec<PriceHistoryPoint>,
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct NegRiskResponse {
556 pub neg_risk: bool,
557}
558
559#[derive(Debug, Clone, Serialize, Deserialize)]
561pub struct FeeRateResponse {
562 pub base_fee: u32,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize)]
567pub struct TickSizeResponse {
568 #[serde(deserialize_with = "deserialize_tick_size")]
569 pub minimum_tick_size: String,
570}
571
572#[derive(Debug, Clone, Serialize)]
574pub struct BookParams {
575 pub token_id: String,
576 #[serde(skip_serializing_if = "Option::is_none")]
577 pub side: Option<OrderSide>,
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize)]
582pub struct SpreadResponse {
583 pub token_id: Option<String>,
584 pub spread: String,
585 pub bid: Option<String>,
586 pub ask: Option<String>,
587}
588
589#[derive(Debug, Clone, Serialize, Deserialize)]
591pub struct LastTradePriceResponse {
592 pub token_id: Option<String>,
593 pub price: Option<String>,
594 pub last_trade_price: Option<String>,
595 pub side: Option<String>,
596 pub timestamp: Option<String>,
597}
598
599#[derive(Debug, Clone, Serialize)]
601pub struct CalculatePriceParams {
602 pub token_id: String,
603 pub side: OrderSide,
604 pub amount: String,
605}
606
607#[derive(Debug, Clone, Serialize, Deserialize)]
609pub struct CalculatePriceResponse {
610 pub price: String,
611}
612
613fn deserialize_tick_size<'de, D>(deserializer: D) -> Result<String, D::Error>
614where
615 D: serde::Deserializer<'de>,
616{
617 use serde::Deserialize;
618 let v = serde_json::Value::deserialize(deserializer)?;
619 match v {
620 serde_json::Value::String(s) => Ok(s),
621 serde_json::Value::Number(n) => Ok(n.to_string()),
622 _ => Err(serde::de::Error::custom(
623 "expected string or number for tick size",
624 )),
625 }
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize)]
633pub struct ClobToken {
634 pub t: String,
636 pub o: String,
638}
639
640#[derive(Debug, Clone, Serialize, Deserialize)]
645pub struct FeeDetails {
646 pub r: Option<f64>,
648 pub e: Option<f64>,
650 pub to: Option<bool>,
652}
653
654#[derive(Debug, Clone, Default, Serialize, Deserialize)]
659#[serde(transparent)]
660pub struct ClobRewards {
661 pub extra: HashMap<String, serde_json::Value>,
663}
664
665#[derive(Debug, Clone, Serialize, Deserialize)]
675pub struct ClobMarketDetails {
676 pub gst: Option<String>,
678 pub r: ClobRewards,
680 pub t: Vec<ClobToken>,
682 pub mos: f64,
684 pub mts: f64,
686 pub mbf: Option<i64>,
688 pub tbf: Option<i64>,
690 pub rfqe: Option<bool>,
692 pub itode: Option<bool>,
694 pub ibce: bool,
696 pub fd: Option<FeeDetails>,
698 pub oas: Option<i32>,
700}
701
702#[derive(Debug, Clone, Serialize, Deserialize)]
705pub struct MarketByTokenResponse {
706 pub condition_id: String,
708 pub primary_token_id: String,
710 pub secondary_token_id: String,
712}
713
714#[derive(Debug, Clone, Serialize, Deserialize)]
717pub struct LiveActivityMarket {
718 pub condition_id: Option<String>,
720 pub id: Option<i64>,
722 pub question: Option<String>,
724 pub market_slug: Option<String>,
726 pub event_slug: Option<String>,
728 pub series_slug: Option<String>,
730 pub icon: Option<String>,
732 pub image: Option<String>,
734 #[serde(default)]
736 pub tags: Vec<String>,
737}
738
739#[derive(Debug, Clone, Serialize, Deserialize)]
744pub struct MarketPrice {
745 pub t: u32,
747 pub p: f64,
749}
750
751#[derive(Debug, Clone, Default, Serialize, Deserialize)]
753pub struct BatchPricesHistoryRequest {
754 pub markets: Vec<String>,
756 #[serde(skip_serializing_if = "Option::is_none")]
758 pub start_ts: Option<f64>,
759 #[serde(skip_serializing_if = "Option::is_none")]
761 pub end_ts: Option<f64>,
762 #[serde(skip_serializing_if = "Option::is_none")]
764 pub interval: Option<String>,
765 #[serde(skip_serializing_if = "Option::is_none")]
767 pub fidelity: Option<i32>,
768}
769
770#[derive(Debug, Clone, Default, Serialize, Deserialize)]
773pub struct BatchPricesHistoryResponse {
774 pub history: HashMap<String, Vec<MarketPrice>>,
776}
777
778#[cfg(test)]
779mod tests {
780 use super::*;
781
782 #[test]
783 fn test_fee_rate_response_deserializes() {
784 let json = r#"{"base_fee": 100}"#;
785 let resp: FeeRateResponse = serde_json::from_str(json).unwrap();
786 assert_eq!(resp.base_fee, 100);
787 }
788
789 #[test]
790 fn test_fee_rate_response_deserializes_zero() {
791 let json = r#"{"base_fee": 0}"#;
792 let resp: FeeRateResponse = serde_json::from_str(json).unwrap();
793 assert_eq!(resp.base_fee, 0);
794 }
795
796 #[test]
797 fn test_fee_rate_response_rejects_missing_field() {
798 let json = r#"{"feeRate": "100"}"#;
799 let result = serde_json::from_str::<FeeRateResponse>(json);
800 assert!(result.is_err(), "Should reject JSON missing base_fee field");
801 }
802
803 #[test]
804 fn test_fee_rate_response_rejects_empty_json() {
805 let json = r#"{}"#;
806 let result = serde_json::from_str::<FeeRateResponse>(json);
807 assert!(result.is_err(), "Should reject empty JSON object");
808 }
809
810 #[test]
811 fn book_params_serializes() {
812 let params = BookParams {
813 token_id: "token-1".into(),
814 side: Some(OrderSide::Buy),
815 };
816 let json = serde_json::to_value(¶ms).unwrap();
817 assert_eq!(json["token_id"], "token-1");
818 assert_eq!(json["side"], "BUY");
819 }
820
821 #[test]
822 fn book_params_omits_none_side() {
823 let params = BookParams {
824 token_id: "token-1".into(),
825 side: None,
826 };
827 let json = serde_json::to_value(¶ms).unwrap();
828 assert_eq!(json["token_id"], "token-1");
829 assert!(json.get("side").is_none());
830 }
831
832 #[test]
833 fn spread_response_deserializes() {
834 let json = r#"{
835 "token_id": "token-1",
836 "spread": "0.02",
837 "bid": "0.48",
838 "ask": "0.50"
839 }"#;
840 let resp: SpreadResponse = serde_json::from_str(json).unwrap();
841 assert_eq!(resp.token_id.as_deref(), Some("token-1"));
842 assert_eq!(resp.spread, "0.02");
843 assert_eq!(resp.bid.as_deref(), Some("0.48"));
844 assert_eq!(resp.ask.as_deref(), Some("0.50"));
845 }
846
847 #[test]
848 fn last_trade_price_response_deserializes() {
849 let json = r#"{
850 "token_id": "token-1",
851 "last_trade_price": "0.55",
852 "timestamp": "1700000000"
853 }"#;
854 let resp: LastTradePriceResponse = serde_json::from_str(json).unwrap();
855 assert_eq!(resp.token_id.as_deref(), Some("token-1"));
856 assert_eq!(resp.last_trade_price.as_deref(), Some("0.55"));
857 assert_eq!(resp.timestamp.as_deref(), Some("1700000000"));
858 }
859
860 #[test]
861 fn calculate_price_params_serializes() {
862 let params = CalculatePriceParams {
863 token_id: "token-1".into(),
864 side: OrderSide::Buy,
865 amount: "100.0".into(),
866 };
867 let json = serde_json::to_value(¶ms).unwrap();
868 assert_eq!(json["token_id"], "token-1");
869 assert_eq!(json["side"], "BUY");
870 assert_eq!(json["amount"], "100.0");
871 }
872
873 #[test]
874 fn calculate_price_response_deserializes() {
875 let json = r#"{"price": "0.52"}"#;
876 let resp: CalculatePriceResponse = serde_json::from_str(json).unwrap();
877 assert_eq!(resp.price, "0.52");
878 }
879
880 #[test]
881 fn order_book_deserializes_with_new_fields() {
882 let json = r#"{
883 "market": "0xcond",
884 "asset_id": "0xtoken",
885 "bids": [{"price": "0.48", "size": "100"}],
886 "asks": [{"price": "0.52", "size": "200"}],
887 "timestamp": "1700000000",
888 "hash": "abc123",
889 "min_order_size": "5",
890 "tick_size": "0.001",
891 "neg_risk": false,
892 "last_trade_price": "0.50"
893 }"#;
894 let ob: OrderBook = serde_json::from_str(json).unwrap();
895 assert_eq!(ob.market, "0xcond");
896 assert_eq!(ob.bids.len(), 1);
897 assert_eq!(ob.asks.len(), 1);
898 assert_eq!(ob.min_order_size.as_deref(), Some("5"));
899 assert_eq!(ob.tick_size.as_deref(), Some("0.001"));
900 assert_eq!(ob.neg_risk, Some(false));
901 assert_eq!(ob.last_trade_price.as_deref(), Some("0.50"));
902 }
903
904 #[test]
905 fn order_book_deserializes_without_new_fields() {
906 let json = r#"{
907 "market": "0xcond",
908 "asset_id": "0xtoken",
909 "bids": [],
910 "asks": [],
911 "timestamp": "1700000000",
912 "hash": "abc123"
913 }"#;
914 let ob: OrderBook = serde_json::from_str(json).unwrap();
915 assert_eq!(ob.market, "0xcond");
916 assert!(ob.min_order_size.is_none());
917 assert!(ob.tick_size.is_none());
918 assert!(ob.neg_risk.is_none());
919 assert!(ob.last_trade_price.is_none());
920 }
921
922 #[test]
923 fn clob_market_details_roundtrip() {
924 let json = r#"{
926 "gst": null,
927 "r": {"minSize": 100, "maxSpread": 2.0},
928 "t": [
929 {"t": "71321045679252212594626385532706912750332728571942532289631379312455583992563", "o": "Yes"},
930 {"t": "52114319501245915516055106046884209969926127482827954674443846427813813222426", "o": "No"}
931 ],
932 "mos": 5.0,
933 "mts": 0.01,
934 "mbf": 0,
935 "tbf": 0,
936 "rfqe": true,
937 "itode": false,
938 "ibce": true,
939 "fd": {"r": 0.02, "e": 2.0, "to": true},
940 "oas": 0
941 }"#;
942 let parsed: ClobMarketDetails = serde_json::from_str(json).unwrap();
943 assert!(parsed.gst.is_none());
944 assert_eq!(parsed.t.len(), 2);
945 assert_eq!(parsed.t[0].o, "Yes");
946 assert!((parsed.mos - 5.0).abs() < f64::EPSILON);
947 assert!((parsed.mts - 0.01).abs() < f64::EPSILON);
948 assert_eq!(parsed.mbf, Some(0));
949 assert_eq!(parsed.tbf, Some(0));
950 assert_eq!(parsed.rfqe, Some(true));
951 assert_eq!(parsed.itode, Some(false));
952 assert!(parsed.ibce);
953 let fd = parsed.fd.as_ref().expect("fd present in full payload");
954 assert_eq!(fd.r, Some(0.02));
955 assert_eq!(fd.e, Some(2.0));
956 assert_eq!(fd.to, Some(true));
957 assert_eq!(parsed.oas, Some(0));
958 assert!(parsed.r.extra.contains_key("minSize"));
960
961 let back = serde_json::to_value(&parsed).unwrap();
963 let again: ClobMarketDetails = serde_json::from_value(back).unwrap();
964 assert_eq!(again.t.len(), 2);
965 assert_eq!(again.fd.as_ref().unwrap().r, Some(0.02));
966 }
967
968 #[test]
969 fn clob_market_details_resolved_market_deserializes() {
970 let json = r#"{
975 "c": false,
976 "cbos": false,
977 "gst": null,
978 "ibce": true,
979 "mos": 5.0,
980 "mts": 0.01,
981 "r": {},
982 "sd": false,
983 "t": [
984 {"t": "713210456", "o": "Yes"},
985 {"t": "521143195", "o": "No"}
986 ]
987 }"#;
988 let parsed: ClobMarketDetails = serde_json::from_str(json)
989 .expect("resolved market (reduced payload) should deserialize");
990 assert_eq!(parsed.t.len(), 2);
991 assert!(parsed.ibce);
992 assert!(parsed.mbf.is_none());
993 assert!(parsed.tbf.is_none());
994 assert!(parsed.rfqe.is_none());
995 assert!(parsed.itode.is_none());
996 assert!(parsed.fd.is_none());
997 assert!(parsed.oas.is_none());
998 }
999
1000 #[test]
1001 fn market_by_token_response_deserializes() {
1002 let json = r#"{
1003 "condition_id": "0xbd31dc8a",
1004 "primary_token_id": "713210456",
1005 "secondary_token_id": "521143195"
1006 }"#;
1007 let parsed: MarketByTokenResponse = serde_json::from_str(json).unwrap();
1008 assert_eq!(parsed.condition_id, "0xbd31dc8a");
1009 assert_eq!(parsed.primary_token_id, "713210456");
1010 assert_eq!(parsed.secondary_token_id, "521143195");
1011 }
1012
1013 #[test]
1014 fn live_activity_market_roundtrip() {
1015 let json = r#"{
1016 "condition_id": "0xcond",
1017 "id": 42,
1018 "question": "Will X happen?",
1019 "market_slug": "will-x-happen",
1020 "event_slug": "x-event",
1021 "series_slug": null,
1022 "icon": "https://icon",
1023 "image": "https://image",
1024 "tags": ["crypto", "sports"]
1025 }"#;
1026 let parsed: LiveActivityMarket = serde_json::from_str(json).unwrap();
1027 assert_eq!(parsed.condition_id.as_deref(), Some("0xcond"));
1028 assert_eq!(parsed.id, Some(42));
1029 assert_eq!(parsed.question.as_deref(), Some("Will X happen?"));
1030 assert_eq!(parsed.market_slug.as_deref(), Some("will-x-happen"));
1031 assert_eq!(parsed.event_slug.as_deref(), Some("x-event"));
1032 assert!(parsed.series_slug.is_none());
1033 assert_eq!(parsed.tags, vec!["crypto", "sports"]);
1034
1035 let back: LiveActivityMarket =
1037 serde_json::from_value(serde_json::to_value(&parsed).unwrap()).unwrap();
1038 assert_eq!(back.id, Some(42));
1039 }
1040
1041 #[test]
1042 fn batch_prices_history_request_omits_none_fields() {
1043 let req = BatchPricesHistoryRequest {
1044 markets: vec!["0xtoken1".into(), "0xtoken2".into()],
1045 start_ts: Some(1_700_000_000.0),
1046 end_ts: None,
1047 interval: Some("1d".into()),
1048 fidelity: None,
1049 };
1050 let json = serde_json::to_value(&req).unwrap();
1051 assert_eq!(json["markets"][0], "0xtoken1");
1052 assert!((json["start_ts"].as_f64().unwrap() - 1_700_000_000.0).abs() < f64::EPSILON);
1053 assert_eq!(json["interval"], "1d");
1054 assert!(json.get("end_ts").is_none());
1055 assert!(json.get("fidelity").is_none());
1056 }
1057
1058 #[test]
1059 fn batch_prices_history_response_roundtrip() {
1060 let json = r#"{
1061 "history": {
1062 "0xtokenA": [
1063 {"t": 1700000000, "p": 0.55},
1064 {"t": 1700001000, "p": 0.60}
1065 ],
1066 "0xtokenB": [
1067 {"t": 1700000000, "p": 0.30}
1068 ]
1069 }
1070 }"#;
1071 let parsed: BatchPricesHistoryResponse = serde_json::from_str(json).unwrap();
1072 assert_eq!(parsed.history.len(), 2);
1073 let a = parsed.history.get("0xtokenA").unwrap();
1074 assert_eq!(a.len(), 2);
1075 assert_eq!(a[0].t, 1_700_000_000);
1076 assert!((a[0].p - 0.55).abs() < f64::EPSILON);
1077 let b = parsed.history.get("0xtokenB").unwrap();
1078 assert_eq!(b.len(), 1);
1079 assert!((b[0].p - 0.30).abs() < f64::EPSILON);
1080
1081 let back: BatchPricesHistoryResponse =
1083 serde_json::from_value(serde_json::to_value(&parsed).unwrap()).unwrap();
1084 assert_eq!(back.history.len(), 2);
1085 }
1086}