1use polyoxide_core::{
2 HttpClient, HttpClientBuilder, RateLimiter, RetryConfig, DEFAULT_POOL_SIZE, DEFAULT_TIMEOUT_MS,
3};
4
5use crate::{
6 api::{
7 accounting::AccountingApi,
8 builders::BuildersApi,
9 combos::CombosApi,
10 health::Health,
11 holders::Holders,
12 leaderboard::LeaderboardApi,
13 live_volume::LiveVolumeApi,
14 market_positions::MarketPositionsApi,
15 misc::MiscApi,
16 open_interest::OpenInterestApi,
17 pnl::PnlApi,
18 rankings::RankingsApi,
19 trades::Trades,
20 users::{UserApi, UserTraded},
21 },
22 error::DataApiError,
23};
24
25const DEFAULT_BASE_URL: &str = "https://data-api.polymarket.com";
26const DEFAULT_PNL_BASE_URL: &str = "https://user-pnl-api.polymarket.com";
27const DEFAULT_RANKINGS_BASE_URL: &str = "https://lb-api.polymarket.com";
28
29#[derive(Clone)]
37pub struct DataApi {
38 pub(crate) http_client: HttpClient,
39 pub(crate) pnl_http_client: HttpClient,
40 pub(crate) rankings_http_client: HttpClient,
41}
42
43impl DataApi {
44 pub fn new() -> Result<Self, DataApiError> {
46 Self::builder().build()
47 }
48
49 pub fn builder() -> DataApiBuilder {
51 DataApiBuilder::new()
52 }
53
54 pub fn health(&self) -> Health {
56 Health {
57 http_client: self.http_client.clone(),
58 }
59 }
60
61 pub fn user(&self, user_address: impl Into<String>) -> UserApi {
63 UserApi {
64 http_client: self.http_client.clone(),
65 user_address: user_address.into(),
66 }
67 }
68
69 pub fn positions(&self, user_address: impl Into<String>) -> UserApi {
71 self.user(user_address)
72 }
73
74 pub fn traded(&self, user_address: impl Into<String>) -> Traded {
76 Traded {
77 user_api: self.user(user_address),
78 }
79 }
80
81 pub fn trades(&self) -> Trades {
83 Trades {
84 http_client: self.http_client.clone(),
85 }
86 }
87
88 pub fn holders(&self) -> Holders {
90 Holders {
91 http_client: self.http_client.clone(),
92 }
93 }
94
95 pub fn open_interest(&self) -> OpenInterestApi {
97 OpenInterestApi {
98 http_client: self.http_client.clone(),
99 }
100 }
101
102 pub fn live_volume(&self) -> LiveVolumeApi {
104 LiveVolumeApi {
105 http_client: self.http_client.clone(),
106 }
107 }
108
109 pub fn builders(&self) -> BuildersApi {
111 BuildersApi {
112 http_client: self.http_client.clone(),
113 }
114 }
115
116 pub fn leaderboard(&self) -> LeaderboardApi {
118 LeaderboardApi {
119 http_client: self.http_client.clone(),
120 }
121 }
122
123 pub fn market_positions(&self) -> MarketPositionsApi {
125 MarketPositionsApi {
126 http_client: self.http_client.clone(),
127 }
128 }
129
130 pub fn accounting(&self) -> AccountingApi {
132 AccountingApi {
133 http_client: self.http_client.clone(),
134 }
135 }
136
137 pub fn combos(&self) -> CombosApi {
139 CombosApi {
140 http_client: self.http_client.clone(),
141 }
142 }
143
144 pub fn misc(&self) -> MiscApi {
146 MiscApi {
147 http_client: self.http_client.clone(),
148 }
149 }
150
151 pub fn pnl(&self) -> PnlApi {
156 PnlApi {
157 http_client: self.pnl_http_client.clone(),
158 }
159 }
160
161 pub fn rankings(&self) -> RankingsApi {
167 RankingsApi {
168 http_client: self.rankings_http_client.clone(),
169 }
170 }
171}
172
173pub struct DataApiBuilder {
175 base_url: String,
176 pnl_base_url: String,
177 rankings_base_url: String,
178 timeout_ms: u64,
179 pool_size: usize,
180 retry_config: Option<RetryConfig>,
181 max_concurrent: Option<usize>,
182}
183
184impl DataApiBuilder {
185 fn new() -> Self {
186 Self {
187 base_url: DEFAULT_BASE_URL.to_string(),
188 pnl_base_url: DEFAULT_PNL_BASE_URL.to_string(),
189 rankings_base_url: DEFAULT_RANKINGS_BASE_URL.to_string(),
190 timeout_ms: DEFAULT_TIMEOUT_MS,
191 pool_size: DEFAULT_POOL_SIZE,
192 retry_config: None,
193 max_concurrent: None,
194 }
195 }
196
197 pub fn base_url(mut self, url: impl Into<String>) -> Self {
203 self.base_url = url.into();
204 self
205 }
206
207 pub fn pnl_base_url(mut self, url: impl Into<String>) -> Self {
214 self.pnl_base_url = url.into();
215 self
216 }
217
218 pub fn rankings_base_url(mut self, url: impl Into<String>) -> Self {
225 self.rankings_base_url = url.into();
226 self
227 }
228
229 pub fn timeout_ms(mut self, timeout: u64) -> Self {
231 self.timeout_ms = timeout;
232 self
233 }
234
235 pub fn pool_size(mut self, size: usize) -> Self {
237 self.pool_size = size;
238 self
239 }
240
241 pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
243 self.retry_config = Some(config);
244 self
245 }
246
247 pub fn max_concurrent(mut self, max: usize) -> Self {
251 self.max_concurrent = Some(max);
252 self
253 }
254
255 pub fn build(self) -> Result<DataApi, DataApiError> {
257 let mut builder = HttpClientBuilder::new(&self.base_url)
258 .timeout_ms(self.timeout_ms)
259 .pool_size(self.pool_size)
260 .with_rate_limiter(RateLimiter::data_default())
261 .with_max_concurrent(self.max_concurrent.unwrap_or(4));
262 if let Some(config) = self.retry_config {
263 builder = builder.with_retry_config(config);
264 }
265 let http_client = builder.build()?;
266
267 let pnl_http_client = http_client.with_base_url(&self.pnl_base_url)?;
270 let rankings_http_client = http_client.with_base_url(&self.rankings_base_url)?;
271
272 Ok(DataApi {
273 http_client,
274 pnl_http_client,
275 rankings_http_client,
276 })
277 }
278}
279
280impl Default for DataApiBuilder {
281 fn default() -> Self {
282 Self::new()
283 }
284}
285
286pub struct Traded {
288 user_api: UserApi,
289}
290
291impl Traded {
292 pub async fn get(self) -> std::result::Result<UserTraded, DataApiError> {
294 self.user_api.traded().await
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn test_builder_default() {
304 let builder = DataApiBuilder::default();
305 assert_eq!(builder.base_url, DEFAULT_BASE_URL);
306 }
307
308 #[test]
309 fn test_builder_custom_retry_config() {
310 let config = RetryConfig {
311 max_retries: 5,
312 initial_backoff_ms: 1000,
313 max_backoff_ms: 30_000,
314 };
315 let builder = DataApiBuilder::new().with_retry_config(config);
316 let config = builder.retry_config.unwrap();
317 assert_eq!(config.max_retries, 5);
318 assert_eq!(config.initial_backoff_ms, 1000);
319 }
320
321 #[test]
322 fn test_builder_custom_max_concurrent() {
323 let builder = DataApiBuilder::new().max_concurrent(10);
324 assert_eq!(builder.max_concurrent, Some(10));
325 }
326
327 #[tokio::test]
328 async fn test_default_concurrency_limit_is_4() {
329 let data = DataApi::new().unwrap();
330 let mut permits = Vec::new();
331 for _ in 0..4 {
332 permits.push(data.http_client.acquire_concurrency().await);
333 }
334 assert!(permits.iter().all(|p| p.is_some()));
335
336 let result = tokio::time::timeout(
337 std::time::Duration::from_millis(50),
338 data.http_client.acquire_concurrency(),
339 )
340 .await;
341 assert!(
342 result.is_err(),
343 "5th permit should block with default limit of 4"
344 );
345 }
346
347 #[test]
348 fn test_builder_build_success() {
349 let data = DataApi::builder().build();
350 assert!(data.is_ok());
351 }
352}