1use polyoxide_core::{HttpClient, QueryBuilder};
2use rust_decimal::Decimal;
3use serde::{Deserialize, Serialize};
4
5use crate::{
6 request::{AuthMode, Request},
7 types::OrderSide,
8};
9
10#[derive(Clone)]
12pub struct Markets {
13 pub(crate) http_client: HttpClient,
14 pub(crate) chain_id: u64,
15}
16
17impl Markets {
18 pub fn get(&self, condition_id: impl Into<String>) -> Request<Market> {
20 Request::get(
21 self.http_client.clone(),
22 format!("/markets/{}", urlencoding::encode(&condition_id.into())),
23 AuthMode::None,
24 self.chain_id,
25 )
26 }
27
28 pub fn get_by_token_ids(
29 &self,
30 token_ids: impl Into<Vec<String>>,
31 ) -> Request<ListMarketsResponse> {
32 Request::get(
33 self.http_client.clone(),
34 "/markets",
35 AuthMode::None,
36 self.chain_id,
37 )
38 .query_many("clob_token_ids", token_ids.into())
39 }
40
41 pub fn list(&self) -> Request<ListMarketsResponse> {
43 Request::get(
44 self.http_client.clone(),
45 "/markets",
46 AuthMode::None,
47 self.chain_id,
48 )
49 }
50
51 pub fn order_book(&self, token_id: impl Into<String>) -> Request<OrderBook> {
53 Request::get(
54 self.http_client.clone(),
55 "/book",
56 AuthMode::None,
57 self.chain_id,
58 )
59 .query("token_id", token_id.into())
60 }
61
62 pub fn price(&self, token_id: impl Into<String>, side: OrderSide) -> Request<PriceResponse> {
64 Request::get(
65 self.http_client.clone(),
66 "/price",
67 AuthMode::None,
68 self.chain_id,
69 )
70 .query("token_id", token_id.into())
71 .query("side", side.as_str())
72 }
73
74 pub fn midpoint(&self, token_id: impl Into<String>) -> Request<MidpointResponse> {
76 Request::get(
77 self.http_client.clone(),
78 "/midpoint",
79 AuthMode::None,
80 self.chain_id,
81 )
82 .query("token_id", token_id.into())
83 }
84
85 pub fn prices_history(&self, token_id: impl Into<String>) -> Request<PricesHistoryResponse> {
87 Request::get(
88 self.http_client.clone(),
89 "/prices-history",
90 AuthMode::None,
91 self.chain_id,
92 )
93 .query("market", token_id.into())
94 }
95
96 pub fn neg_risk(&self, token_id: impl Into<String>) -> Request<NegRiskResponse> {
98 Request::get(
99 self.http_client.clone(),
100 "/neg-risk".to_string(),
101 AuthMode::None,
102 self.chain_id,
103 )
104 .query("token_id", token_id.into())
105 }
106
107 pub fn fee_rate(&self, token_id: impl Into<String>) -> Request<FeeRateResponse> {
109 Request::get(
110 self.http_client.clone(),
111 "/fee-rate",
112 AuthMode::None,
113 self.chain_id,
114 )
115 .query("token_id", token_id.into())
116 }
117
118 pub fn tick_size(&self, token_id: impl Into<String>) -> Request<TickSizeResponse> {
120 Request::get(
121 self.http_client.clone(),
122 "/tick-size".to_string(),
123 AuthMode::None,
124 self.chain_id,
125 )
126 .query("token_id", token_id.into())
127 }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct Market {
133 pub condition_id: String,
134 pub question_id: String,
135 pub tokens: Vec<MarketToken>,
136 pub rewards: Option<serde_json::Value>,
137 pub minimum_order_size: f64,
138 pub minimum_tick_size: f64,
139 pub description: String,
140 pub category: Option<String>,
141 pub end_date_iso: Option<String>,
142 pub question: String,
143 pub active: bool,
144 pub closed: bool,
145 pub archived: bool,
146 pub neg_risk: Option<bool>,
147 pub neg_risk_market_id: Option<String>,
148 pub enable_order_book: Option<bool>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct ListMarketsResponse {
154 pub data: Vec<Market>,
155 pub next_cursor: Option<String>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct MarketToken {
161 pub token_id: Option<String>,
162 pub outcome: String,
163 pub price: Option<f64>,
164 pub winner: Option<bool>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct OrderLevel {
170 #[serde(with = "rust_decimal::serde::str")]
171 pub price: Decimal,
172 #[serde(with = "rust_decimal::serde::str")]
173 pub size: Decimal,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct OrderBook {
179 pub market: String,
180 pub asset_id: String,
181 pub bids: Vec<OrderLevel>,
182 pub asks: Vec<OrderLevel>,
183 pub timestamp: String,
184 pub hash: String,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct PriceResponse {
190 pub price: String,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct MidpointResponse {
196 pub mid: String,
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct PriceHistoryPoint {
202 #[serde(rename = "t")]
204 pub timestamp: i64,
205 #[serde(rename = "p")]
207 pub price: f64,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct PricesHistoryResponse {
213 pub history: Vec<PriceHistoryPoint>,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize)]
218pub struct NegRiskResponse {
219 pub neg_risk: bool,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct FeeRateResponse {
225 pub base_fee: u32,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct TickSizeResponse {
231 #[serde(deserialize_with = "deserialize_tick_size")]
232 pub minimum_tick_size: String,
233}
234
235fn deserialize_tick_size<'de, D>(deserializer: D) -> Result<String, D::Error>
236where
237 D: serde::Deserializer<'de>,
238{
239 use serde::Deserialize;
240 let v = serde_json::Value::deserialize(deserializer)?;
241 match v {
242 serde_json::Value::String(s) => Ok(s),
243 serde_json::Value::Number(n) => Ok(n.to_string()),
244 _ => Err(serde::de::Error::custom(
245 "expected string or number for tick size",
246 )),
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_fee_rate_response_deserializes() {
256 let json = r#"{"base_fee": 100}"#;
257 let resp: FeeRateResponse = serde_json::from_str(json).unwrap();
258 assert_eq!(resp.base_fee, 100);
259 }
260
261 #[test]
262 fn test_fee_rate_response_deserializes_zero() {
263 let json = r#"{"base_fee": 0}"#;
264 let resp: FeeRateResponse = serde_json::from_str(json).unwrap();
265 assert_eq!(resp.base_fee, 0);
266 }
267
268 #[test]
269 fn test_fee_rate_response_rejects_missing_field() {
270 let json = r#"{"feeRate": "100"}"#;
271 let result = serde_json::from_str::<FeeRateResponse>(json);
272 assert!(result.is_err(), "Should reject JSON missing base_fee field");
273 }
274
275 #[test]
276 fn test_fee_rate_response_rejects_empty_json() {
277 let json = r#"{}"#;
278 let result = serde_json::from_str::<FeeRateResponse>(json);
279 assert!(result.is_err(), "Should reject empty JSON object");
280 }
281}