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 Request::get(
91 self.http_client.clone(),
92 "/prices-history",
93 AuthMode::None,
94 self.chain_id,
95 )
96 .query("market", token_id.into())
97 }
98
99 pub fn neg_risk(&self, token_id: impl Into<String>) -> Request<NegRiskResponse> {
101 Request::get(
102 self.http_client.clone(),
103 "/neg-risk".to_string(),
104 AuthMode::None,
105 self.chain_id,
106 )
107 .query("token_id", token_id.into())
108 }
109
110 pub fn fee_rate(&self, token_id: impl Into<String>) -> Request<FeeRateResponse> {
112 Request::get(
113 self.http_client.clone(),
114 "/fee-rate",
115 AuthMode::None,
116 self.chain_id,
117 )
118 .query("token_id", token_id.into())
119 }
120
121 pub fn tick_size(&self, token_id: impl Into<String>) -> Request<TickSizeResponse> {
123 Request::get(
124 self.http_client.clone(),
125 "/tick-size".to_string(),
126 AuthMode::None,
127 self.chain_id,
128 )
129 .query("token_id", token_id.into())
130 }
131
132 pub fn neg_risk_path(&self, token_id: impl Into<String>) -> Request<NegRiskResponse> {
134 Request::get(
135 self.http_client.clone(),
136 format!("/neg-risk/{}", urlencoding::encode(&token_id.into())),
137 AuthMode::None,
138 self.chain_id,
139 )
140 }
141
142 pub fn fee_rate_path(&self, token_id: impl Into<String>) -> Request<FeeRateResponse> {
144 Request::get(
145 self.http_client.clone(),
146 format!("/fee-rate/{}", urlencoding::encode(&token_id.into())),
147 AuthMode::None,
148 self.chain_id,
149 )
150 }
151
152 pub fn tick_size_path(&self, token_id: impl Into<String>) -> Request<TickSizeResponse> {
154 Request::get(
155 self.http_client.clone(),
156 format!("/tick-size/{}", urlencoding::encode(&token_id.into())),
157 AuthMode::None,
158 self.chain_id,
159 )
160 }
161
162 pub fn clob_market_details(
167 &self,
168 condition_id: impl Into<String>,
169 ) -> Request<ClobMarketDetails> {
170 Request::get(
171 self.http_client.clone(),
172 format!(
173 "/clob-markets/{}",
174 urlencoding::encode(&condition_id.into())
175 ),
176 AuthMode::None,
177 self.chain_id,
178 )
179 }
180
181 pub fn market_by_token(&self, token_id: impl Into<String>) -> Request<MarketByTokenResponse> {
186 Request::get(
187 self.http_client.clone(),
188 format!(
189 "/markets-by-token/{}",
190 urlencoding::encode(&token_id.into())
191 ),
192 AuthMode::None,
193 self.chain_id,
194 )
195 }
196
197 pub fn live_activity_market(
200 &self,
201 condition_id: impl Into<String>,
202 ) -> Request<LiveActivityMarket> {
203 Request::get(
204 self.http_client.clone(),
205 format!(
206 "/markets/live-activity/{}",
207 urlencoding::encode(&condition_id.into())
208 ),
209 AuthMode::None,
210 self.chain_id,
211 )
212 }
213
214 pub fn live_activity_bulk(
217 &self,
218 condition_ids: Vec<String>,
219 ) -> Result<Request<Vec<LiveActivityMarket>>, ClobError> {
220 Request::<Vec<LiveActivityMarket>>::post(
221 self.http_client.clone(),
222 "/markets/live-activity".to_string(),
223 AuthMode::None,
224 self.chain_id,
225 )
226 .body(&condition_ids)
227 }
228
229 pub fn batch_prices_history(
232 &self,
233 req: &BatchPricesHistoryRequest,
234 ) -> Result<Request<BatchPricesHistoryResponse>, ClobError> {
235 Request::<BatchPricesHistoryResponse>::post(
236 self.http_client.clone(),
237 "/batch-prices-history".to_string(),
238 AuthMode::None,
239 self.chain_id,
240 )
241 .body(req)
242 }
243
244 pub fn spread(&self, token_id: impl Into<String>) -> Request<SpreadResponse> {
246 Request::get(
247 self.http_client.clone(),
248 "/spread",
249 AuthMode::None,
250 self.chain_id,
251 )
252 .query("token_id", token_id.into())
253 }
254
255 pub fn last_trade_price(&self, token_id: impl Into<String>) -> Request<LastTradePriceResponse> {
257 Request::get(
258 self.http_client.clone(),
259 "/last-trade-price",
260 AuthMode::None,
261 self.chain_id,
262 )
263 .query("token_id", token_id.into())
264 }
265
266 pub fn live_activity(
268 &self,
269 condition_id: impl Into<String>,
270 ) -> Request<Vec<LiveActivityEvent>> {
271 Request::get(
272 self.http_client.clone(),
273 format!(
274 "/live-activity/events/{}",
275 urlencoding::encode(&condition_id.into())
276 ),
277 AuthMode::None,
278 self.chain_id,
279 )
280 }
281
282 pub fn simplified(&self) -> Request<ListMarketsResponse> {
284 Request::get(
285 self.http_client.clone(),
286 "/simplified-markets",
287 AuthMode::None,
288 self.chain_id,
289 )
290 }
291
292 pub fn sampling(&self) -> Request<ListMarketsResponse> {
294 Request::get(
295 self.http_client.clone(),
296 "/sampling-markets",
297 AuthMode::None,
298 self.chain_id,
299 )
300 }
301
302 pub fn sampling_simplified(&self) -> Request<ListMarketsResponse> {
304 Request::get(
305 self.http_client.clone(),
306 "/sampling-simplified-markets",
307 AuthMode::None,
308 self.chain_id,
309 )
310 }
311
312 pub async fn calculate_price(
314 &self,
315 token_id: impl Into<String>,
316 side: OrderSide,
317 amount: impl Into<String>,
318 ) -> Result<CalculatePriceResponse, ClobError> {
319 Request::<CalculatePriceResponse>::post(
320 self.http_client.clone(),
321 "/calculate-price".to_string(),
322 AuthMode::None,
323 self.chain_id,
324 )
325 .body(&CalculatePriceParams {
326 token_id: token_id.into(),
327 side,
328 amount: amount.into(),
329 })?
330 .send()
331 .await
332 }
333
334 pub async fn order_books(&self, params: &[BookParams]) -> Result<Vec<OrderBook>, ClobError> {
336 Request::<Vec<OrderBook>>::post(
337 self.http_client.clone(),
338 "/books".to_string(),
339 AuthMode::None,
340 self.chain_id,
341 )
342 .body(params)?
343 .send()
344 .await
345 }
346
347 pub async fn prices(&self, params: &[BookParams]) -> Result<Vec<PriceResponse>, ClobError> {
349 Request::<Vec<PriceResponse>>::post(
350 self.http_client.clone(),
351 "/prices".to_string(),
352 AuthMode::None,
353 self.chain_id,
354 )
355 .body(params)?
356 .send()
357 .await
358 }
359
360 pub async fn midpoints(
362 &self,
363 params: &[BookParams],
364 ) -> Result<Vec<MidpointResponse>, ClobError> {
365 Request::<Vec<MidpointResponse>>::post(
366 self.http_client.clone(),
367 "/midpoints".to_string(),
368 AuthMode::None,
369 self.chain_id,
370 )
371 .body(params)?
372 .send()
373 .await
374 }
375
376 pub async fn spreads(&self, params: &[BookParams]) -> Result<Vec<SpreadResponse>, ClobError> {
378 Request::<Vec<SpreadResponse>>::post(
379 self.http_client.clone(),
380 "/spreads".to_string(),
381 AuthMode::None,
382 self.chain_id,
383 )
384 .body(params)?
385 .send()
386 .await
387 }
388
389 pub async fn last_trade_prices(
391 &self,
392 params: &[BookParams],
393 ) -> Result<Vec<LastTradePriceResponse>, ClobError> {
394 Request::<Vec<LastTradePriceResponse>>::post(
395 self.http_client.clone(),
396 "/last-trades-prices".to_string(),
397 AuthMode::None,
398 self.chain_id,
399 )
400 .body(params)?
401 .send()
402 .await
403 }
404}
405
406#[derive(Debug, Clone, Serialize, Deserialize)]
408pub struct Market {
409 pub condition_id: String,
410 pub question_id: Option<String>,
411 pub tokens: Vec<MarketToken>,
412 pub rewards: Option<serde_json::Value>,
413 pub minimum_order_size: Option<f64>,
414 pub minimum_tick_size: Option<f64>,
415 pub description: Option<String>,
416 pub category: Option<String>,
417 pub end_date_iso: Option<String>,
418 pub question: Option<String>,
419 pub active: bool,
420 pub closed: bool,
421 pub archived: bool,
422 pub accepting_orders: Option<bool>,
423 pub neg_risk: Option<bool>,
424 pub neg_risk_market_id: Option<String>,
425 pub enable_order_book: Option<bool>,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct ListMarketsResponse {
431 pub data: Vec<Market>,
432 pub next_cursor: Option<String>,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct MarketToken {
438 pub token_id: Option<String>,
439 pub outcome: String,
440 pub price: Option<f64>,
441 pub winner: Option<bool>,
442}
443
444#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct OrderLevel {
447 #[serde(with = "rust_decimal::serde::str")]
448 pub price: Decimal,
449 #[serde(with = "rust_decimal::serde::str")]
450 pub size: Decimal,
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct OrderBook {
456 pub market: String,
457 pub asset_id: String,
458 pub bids: Vec<OrderLevel>,
459 pub asks: Vec<OrderLevel>,
460 pub timestamp: String,
461 pub hash: String,
462 pub min_order_size: Option<String>,
463 pub tick_size: Option<String>,
464 #[serde(default)]
465 pub neg_risk: Option<bool>,
466 pub last_trade_price: Option<String>,
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize)]
471pub struct PriceResponse {
472 pub price: String,
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
477pub struct MidpointResponse {
478 pub mid: String,
479}
480
481#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct PriceHistoryPoint {
484 #[serde(rename = "t")]
486 pub timestamp: i64,
487 #[serde(rename = "p")]
489 pub price: f64,
490}
491
492#[derive(Debug, Clone, Serialize, Deserialize)]
494pub struct PricesHistoryResponse {
495 pub history: Vec<PriceHistoryPoint>,
496}
497
498#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct NegRiskResponse {
501 pub neg_risk: bool,
502}
503
504#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct FeeRateResponse {
507 pub base_fee: u32,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize)]
512pub struct TickSizeResponse {
513 #[serde(deserialize_with = "deserialize_tick_size")]
514 pub minimum_tick_size: String,
515}
516
517#[derive(Debug, Clone, Serialize)]
519pub struct BookParams {
520 pub token_id: String,
521 #[serde(skip_serializing_if = "Option::is_none")]
522 pub side: Option<OrderSide>,
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct SpreadResponse {
528 pub token_id: Option<String>,
529 pub spread: String,
530 pub bid: Option<String>,
531 pub ask: Option<String>,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize)]
536pub struct LastTradePriceResponse {
537 pub token_id: Option<String>,
538 pub price: Option<String>,
539 pub last_trade_price: Option<String>,
540 pub side: Option<String>,
541 pub timestamp: Option<String>,
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct LiveActivityEvent {
547 pub condition_id: String,
548 #[serde(flatten)]
549 pub extra: serde_json::Value,
550}
551
552#[derive(Debug, Clone, Serialize)]
554pub struct CalculatePriceParams {
555 pub token_id: String,
556 pub side: OrderSide,
557 pub amount: String,
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize)]
562pub struct CalculatePriceResponse {
563 pub price: String,
564}
565
566fn deserialize_tick_size<'de, D>(deserializer: D) -> Result<String, D::Error>
567where
568 D: serde::Deserializer<'de>,
569{
570 use serde::Deserialize;
571 let v = serde_json::Value::deserialize(deserializer)?;
572 match v {
573 serde_json::Value::String(s) => Ok(s),
574 serde_json::Value::Number(n) => Ok(n.to_string()),
575 _ => Err(serde::de::Error::custom(
576 "expected string or number for tick size",
577 )),
578 }
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
586pub struct ClobToken {
587 pub t: String,
589 pub o: String,
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize)]
598pub struct FeeDetails {
599 pub r: Option<f64>,
601 pub e: Option<f64>,
603 pub to: Option<bool>,
605}
606
607#[derive(Debug, Clone, Default, Serialize, Deserialize)]
612#[serde(transparent)]
613pub struct ClobRewards {
614 pub extra: HashMap<String, serde_json::Value>,
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize)]
628pub struct ClobMarketDetails {
629 pub gst: Option<String>,
631 pub r: ClobRewards,
633 pub t: Vec<ClobToken>,
635 pub mos: f64,
637 pub mts: f64,
639 pub mbf: i64,
641 pub tbf: i64,
643 pub rfqe: bool,
645 pub itode: bool,
647 pub ibce: bool,
649 pub fd: FeeDetails,
651 pub oas: i32,
653}
654
655#[derive(Debug, Clone, Serialize, Deserialize)]
658pub struct MarketByTokenResponse {
659 pub condition_id: String,
661 pub primary_token_id: String,
663 pub secondary_token_id: String,
665}
666
667#[derive(Debug, Clone, Serialize, Deserialize)]
670pub struct LiveActivityMarket {
671 pub condition_id: Option<String>,
673 pub id: Option<i64>,
675 pub question: Option<String>,
677 pub market_slug: Option<String>,
679 pub event_slug: Option<String>,
681 pub series_slug: Option<String>,
683 pub icon: Option<String>,
685 pub image: Option<String>,
687 #[serde(default)]
689 pub tags: Vec<String>,
690}
691
692#[derive(Debug, Clone, Serialize, Deserialize)]
697pub struct MarketPrice {
698 pub t: u32,
700 pub p: f64,
702}
703
704#[derive(Debug, Clone, Default, Serialize, Deserialize)]
706pub struct BatchPricesHistoryRequest {
707 pub markets: Vec<String>,
709 #[serde(skip_serializing_if = "Option::is_none")]
711 pub start_ts: Option<f64>,
712 #[serde(skip_serializing_if = "Option::is_none")]
714 pub end_ts: Option<f64>,
715 #[serde(skip_serializing_if = "Option::is_none")]
717 pub interval: Option<String>,
718 #[serde(skip_serializing_if = "Option::is_none")]
720 pub fidelity: Option<i32>,
721}
722
723#[derive(Debug, Clone, Default, Serialize, Deserialize)]
726pub struct BatchPricesHistoryResponse {
727 pub history: HashMap<String, Vec<MarketPrice>>,
729}
730
731#[cfg(test)]
732mod tests {
733 use super::*;
734
735 #[test]
736 fn test_fee_rate_response_deserializes() {
737 let json = r#"{"base_fee": 100}"#;
738 let resp: FeeRateResponse = serde_json::from_str(json).unwrap();
739 assert_eq!(resp.base_fee, 100);
740 }
741
742 #[test]
743 fn test_fee_rate_response_deserializes_zero() {
744 let json = r#"{"base_fee": 0}"#;
745 let resp: FeeRateResponse = serde_json::from_str(json).unwrap();
746 assert_eq!(resp.base_fee, 0);
747 }
748
749 #[test]
750 fn test_fee_rate_response_rejects_missing_field() {
751 let json = r#"{"feeRate": "100"}"#;
752 let result = serde_json::from_str::<FeeRateResponse>(json);
753 assert!(result.is_err(), "Should reject JSON missing base_fee field");
754 }
755
756 #[test]
757 fn test_fee_rate_response_rejects_empty_json() {
758 let json = r#"{}"#;
759 let result = serde_json::from_str::<FeeRateResponse>(json);
760 assert!(result.is_err(), "Should reject empty JSON object");
761 }
762
763 #[test]
764 fn book_params_serializes() {
765 let params = BookParams {
766 token_id: "token-1".into(),
767 side: Some(OrderSide::Buy),
768 };
769 let json = serde_json::to_value(¶ms).unwrap();
770 assert_eq!(json["token_id"], "token-1");
771 assert_eq!(json["side"], "BUY");
772 }
773
774 #[test]
775 fn book_params_omits_none_side() {
776 let params = BookParams {
777 token_id: "token-1".into(),
778 side: None,
779 };
780 let json = serde_json::to_value(¶ms).unwrap();
781 assert_eq!(json["token_id"], "token-1");
782 assert!(json.get("side").is_none());
783 }
784
785 #[test]
786 fn spread_response_deserializes() {
787 let json = r#"{
788 "token_id": "token-1",
789 "spread": "0.02",
790 "bid": "0.48",
791 "ask": "0.50"
792 }"#;
793 let resp: SpreadResponse = serde_json::from_str(json).unwrap();
794 assert_eq!(resp.token_id.as_deref(), Some("token-1"));
795 assert_eq!(resp.spread, "0.02");
796 assert_eq!(resp.bid.as_deref(), Some("0.48"));
797 assert_eq!(resp.ask.as_deref(), Some("0.50"));
798 }
799
800 #[test]
801 fn last_trade_price_response_deserializes() {
802 let json = r#"{
803 "token_id": "token-1",
804 "last_trade_price": "0.55",
805 "timestamp": "1700000000"
806 }"#;
807 let resp: LastTradePriceResponse = serde_json::from_str(json).unwrap();
808 assert_eq!(resp.token_id.as_deref(), Some("token-1"));
809 assert_eq!(resp.last_trade_price.as_deref(), Some("0.55"));
810 assert_eq!(resp.timestamp.as_deref(), Some("1700000000"));
811 }
812
813 #[test]
814 fn live_activity_event_deserializes_with_extra_fields() {
815 let json = r#"{
816 "condition_id": "0xabc123",
817 "event_type": "trade",
818 "amount": 100
819 }"#;
820 let event: LiveActivityEvent = serde_json::from_str(json).unwrap();
821 assert_eq!(event.condition_id, "0xabc123");
822 assert_eq!(event.extra["event_type"], "trade");
823 assert_eq!(event.extra["amount"], 100);
824 }
825
826 #[test]
827 fn calculate_price_params_serializes() {
828 let params = CalculatePriceParams {
829 token_id: "token-1".into(),
830 side: OrderSide::Buy,
831 amount: "100.0".into(),
832 };
833 let json = serde_json::to_value(¶ms).unwrap();
834 assert_eq!(json["token_id"], "token-1");
835 assert_eq!(json["side"], "BUY");
836 assert_eq!(json["amount"], "100.0");
837 }
838
839 #[test]
840 fn calculate_price_response_deserializes() {
841 let json = r#"{"price": "0.52"}"#;
842 let resp: CalculatePriceResponse = serde_json::from_str(json).unwrap();
843 assert_eq!(resp.price, "0.52");
844 }
845
846 #[test]
847 fn order_book_deserializes_with_new_fields() {
848 let json = r#"{
849 "market": "0xcond",
850 "asset_id": "0xtoken",
851 "bids": [{"price": "0.48", "size": "100"}],
852 "asks": [{"price": "0.52", "size": "200"}],
853 "timestamp": "1700000000",
854 "hash": "abc123",
855 "min_order_size": "5",
856 "tick_size": "0.001",
857 "neg_risk": false,
858 "last_trade_price": "0.50"
859 }"#;
860 let ob: OrderBook = serde_json::from_str(json).unwrap();
861 assert_eq!(ob.market, "0xcond");
862 assert_eq!(ob.bids.len(), 1);
863 assert_eq!(ob.asks.len(), 1);
864 assert_eq!(ob.min_order_size.as_deref(), Some("5"));
865 assert_eq!(ob.tick_size.as_deref(), Some("0.001"));
866 assert_eq!(ob.neg_risk, Some(false));
867 assert_eq!(ob.last_trade_price.as_deref(), Some("0.50"));
868 }
869
870 #[test]
871 fn order_book_deserializes_without_new_fields() {
872 let json = r#"{
873 "market": "0xcond",
874 "asset_id": "0xtoken",
875 "bids": [],
876 "asks": [],
877 "timestamp": "1700000000",
878 "hash": "abc123"
879 }"#;
880 let ob: OrderBook = serde_json::from_str(json).unwrap();
881 assert_eq!(ob.market, "0xcond");
882 assert!(ob.min_order_size.is_none());
883 assert!(ob.tick_size.is_none());
884 assert!(ob.neg_risk.is_none());
885 assert!(ob.last_trade_price.is_none());
886 }
887
888 #[test]
889 fn clob_market_details_roundtrip() {
890 let json = r#"{
892 "gst": null,
893 "r": {"minSize": 100, "maxSpread": 2.0},
894 "t": [
895 {"t": "71321045679252212594626385532706912750332728571942532289631379312455583992563", "o": "Yes"},
896 {"t": "52114319501245915516055106046884209969926127482827954674443846427813813222426", "o": "No"}
897 ],
898 "mos": 5.0,
899 "mts": 0.01,
900 "mbf": 0,
901 "tbf": 0,
902 "rfqe": true,
903 "itode": false,
904 "ibce": true,
905 "fd": {"r": 0.02, "e": 2.0, "to": true},
906 "oas": 0
907 }"#;
908 let parsed: ClobMarketDetails = serde_json::from_str(json).unwrap();
909 assert!(parsed.gst.is_none());
910 assert_eq!(parsed.t.len(), 2);
911 assert_eq!(parsed.t[0].o, "Yes");
912 assert!((parsed.mos - 5.0).abs() < f64::EPSILON);
913 assert!((parsed.mts - 0.01).abs() < f64::EPSILON);
914 assert_eq!(parsed.mbf, 0);
915 assert_eq!(parsed.tbf, 0);
916 assert!(parsed.rfqe);
917 assert!(!parsed.itode);
918 assert!(parsed.ibce);
919 assert_eq!(parsed.fd.r, Some(0.02));
920 assert_eq!(parsed.fd.e, Some(2.0));
921 assert_eq!(parsed.fd.to, Some(true));
922 assert_eq!(parsed.oas, 0);
923 assert!(parsed.r.extra.contains_key("minSize"));
925
926 let back = serde_json::to_value(&parsed).unwrap();
928 let again: ClobMarketDetails = serde_json::from_value(back).unwrap();
929 assert_eq!(again.t.len(), 2);
930 assert_eq!(again.fd.r, Some(0.02));
931 }
932
933 #[test]
934 fn market_by_token_response_deserializes() {
935 let json = r#"{
936 "condition_id": "0xbd31dc8a",
937 "primary_token_id": "713210456",
938 "secondary_token_id": "521143195"
939 }"#;
940 let parsed: MarketByTokenResponse = serde_json::from_str(json).unwrap();
941 assert_eq!(parsed.condition_id, "0xbd31dc8a");
942 assert_eq!(parsed.primary_token_id, "713210456");
943 assert_eq!(parsed.secondary_token_id, "521143195");
944 }
945
946 #[test]
947 fn live_activity_market_roundtrip() {
948 let json = r#"{
949 "condition_id": "0xcond",
950 "id": 42,
951 "question": "Will X happen?",
952 "market_slug": "will-x-happen",
953 "event_slug": "x-event",
954 "series_slug": null,
955 "icon": "https://icon",
956 "image": "https://image",
957 "tags": ["crypto", "sports"]
958 }"#;
959 let parsed: LiveActivityMarket = serde_json::from_str(json).unwrap();
960 assert_eq!(parsed.condition_id.as_deref(), Some("0xcond"));
961 assert_eq!(parsed.id, Some(42));
962 assert_eq!(parsed.question.as_deref(), Some("Will X happen?"));
963 assert_eq!(parsed.market_slug.as_deref(), Some("will-x-happen"));
964 assert_eq!(parsed.event_slug.as_deref(), Some("x-event"));
965 assert!(parsed.series_slug.is_none());
966 assert_eq!(parsed.tags, vec!["crypto", "sports"]);
967
968 let back: LiveActivityMarket =
970 serde_json::from_value(serde_json::to_value(&parsed).unwrap()).unwrap();
971 assert_eq!(back.id, Some(42));
972 }
973
974 #[test]
975 fn batch_prices_history_request_omits_none_fields() {
976 let req = BatchPricesHistoryRequest {
977 markets: vec!["0xtoken1".into(), "0xtoken2".into()],
978 start_ts: Some(1_700_000_000.0),
979 end_ts: None,
980 interval: Some("1d".into()),
981 fidelity: None,
982 };
983 let json = serde_json::to_value(&req).unwrap();
984 assert_eq!(json["markets"][0], "0xtoken1");
985 assert!((json["start_ts"].as_f64().unwrap() - 1_700_000_000.0).abs() < f64::EPSILON);
986 assert_eq!(json["interval"], "1d");
987 assert!(json.get("end_ts").is_none());
988 assert!(json.get("fidelity").is_none());
989 }
990
991 #[test]
992 fn batch_prices_history_response_roundtrip() {
993 let json = r#"{
994 "history": {
995 "0xtokenA": [
996 {"t": 1700000000, "p": 0.55},
997 {"t": 1700001000, "p": 0.60}
998 ],
999 "0xtokenB": [
1000 {"t": 1700000000, "p": 0.30}
1001 ]
1002 }
1003 }"#;
1004 let parsed: BatchPricesHistoryResponse = serde_json::from_str(json).unwrap();
1005 assert_eq!(parsed.history.len(), 2);
1006 let a = parsed.history.get("0xtokenA").unwrap();
1007 assert_eq!(a.len(), 2);
1008 assert_eq!(a[0].t, 1_700_000_000);
1009 assert!((a[0].p - 0.55).abs() < f64::EPSILON);
1010 let b = parsed.history.get("0xtokenB").unwrap();
1011 assert_eq!(b.len(), 1);
1012 assert!((b[0].p - 0.30).abs() < f64::EPSILON);
1013
1014 let back: BatchPricesHistoryResponse =
1016 serde_json::from_value(serde_json::to_value(&parsed).unwrap()).unwrap();
1017 assert_eq!(back.history.len(), 2);
1018 }
1019}