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 {
210 self.pnl_base_url = url.into();
211 self
212 }
213
214 pub fn rankings_base_url(mut self, url: impl Into<String>) -> Self {
217 self.rankings_base_url = url.into();
218 self
219 }
220
221 pub fn timeout_ms(mut self, timeout: u64) -> Self {
223 self.timeout_ms = timeout;
224 self
225 }
226
227 pub fn pool_size(mut self, size: usize) -> Self {
229 self.pool_size = size;
230 self
231 }
232
233 pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
235 self.retry_config = Some(config);
236 self
237 }
238
239 pub fn max_concurrent(mut self, max: usize) -> Self {
243 self.max_concurrent = Some(max);
244 self
245 }
246
247 pub fn build(self) -> Result<DataApi, DataApiError> {
249 let mut builder = HttpClientBuilder::new(&self.base_url)
250 .timeout_ms(self.timeout_ms)
251 .pool_size(self.pool_size)
252 .with_rate_limiter(RateLimiter::data_default())
253 .with_max_concurrent(self.max_concurrent.unwrap_or(4));
254 if let Some(config) = self.retry_config {
255 builder = builder.with_retry_config(config);
256 }
257 let http_client = builder.build()?;
258
259 let pnl_http_client = http_client.with_base_url(&self.pnl_base_url)?;
262 let rankings_http_client = http_client.with_base_url(&self.rankings_base_url)?;
263
264 Ok(DataApi {
265 http_client,
266 pnl_http_client,
267 rankings_http_client,
268 })
269 }
270}
271
272impl Default for DataApiBuilder {
273 fn default() -> Self {
274 Self::new()
275 }
276}
277
278pub struct Traded {
280 user_api: UserApi,
281}
282
283impl Traded {
284 pub async fn get(self) -> std::result::Result<UserTraded, DataApiError> {
286 self.user_api.traded().await
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn test_builder_default() {
296 let builder = DataApiBuilder::default();
297 assert_eq!(builder.base_url, DEFAULT_BASE_URL);
298 }
299
300 #[test]
301 fn test_builder_custom_retry_config() {
302 let config = RetryConfig {
303 max_retries: 5,
304 initial_backoff_ms: 1000,
305 max_backoff_ms: 30_000,
306 };
307 let builder = DataApiBuilder::new().with_retry_config(config);
308 let config = builder.retry_config.unwrap();
309 assert_eq!(config.max_retries, 5);
310 assert_eq!(config.initial_backoff_ms, 1000);
311 }
312
313 #[test]
314 fn test_builder_custom_max_concurrent() {
315 let builder = DataApiBuilder::new().max_concurrent(10);
316 assert_eq!(builder.max_concurrent, Some(10));
317 }
318
319 #[tokio::test]
320 async fn test_default_concurrency_limit_is_4() {
321 let data = DataApi::new().unwrap();
322 let mut permits = Vec::new();
323 for _ in 0..4 {
324 permits.push(data.http_client.acquire_concurrency().await);
325 }
326 assert!(permits.iter().all(|p| p.is_some()));
327
328 let result = tokio::time::timeout(
329 std::time::Duration::from_millis(50),
330 data.http_client.acquire_concurrency(),
331 )
332 .await;
333 assert!(
334 result.is_err(),
335 "5th permit should block with default limit of 4"
336 );
337 }
338
339 #[test]
340 fn test_builder_build_success() {
341 let data = DataApi::builder().build();
342 assert!(data.is_ok());
343 }
344}