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