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 trades::Trades,
18 users::{UserApi, UserTraded},
19 },
20 error::DataApiError,
21};
22
23const DEFAULT_BASE_URL: &str = "https://data-api.polymarket.com";
24
25#[derive(Clone)]
27pub struct DataApi {
28 pub(crate) http_client: HttpClient,
29}
30
31impl DataApi {
32 pub fn new() -> Result<Self, DataApiError> {
34 Self::builder().build()
35 }
36
37 pub fn builder() -> DataApiBuilder {
39 DataApiBuilder::new()
40 }
41
42 pub fn health(&self) -> Health {
44 Health {
45 http_client: self.http_client.clone(),
46 }
47 }
48
49 pub fn user(&self, user_address: impl Into<String>) -> UserApi {
51 UserApi {
52 http_client: self.http_client.clone(),
53 user_address: user_address.into(),
54 }
55 }
56
57 pub fn positions(&self, user_address: impl Into<String>) -> UserApi {
59 self.user(user_address)
60 }
61
62 pub fn traded(&self, user_address: impl Into<String>) -> Traded {
64 Traded {
65 user_api: self.user(user_address),
66 }
67 }
68
69 pub fn trades(&self) -> Trades {
71 Trades {
72 http_client: self.http_client.clone(),
73 }
74 }
75
76 pub fn holders(&self) -> Holders {
78 Holders {
79 http_client: self.http_client.clone(),
80 }
81 }
82
83 pub fn open_interest(&self) -> OpenInterestApi {
85 OpenInterestApi {
86 http_client: self.http_client.clone(),
87 }
88 }
89
90 pub fn live_volume(&self) -> LiveVolumeApi {
92 LiveVolumeApi {
93 http_client: self.http_client.clone(),
94 }
95 }
96
97 pub fn builders(&self) -> BuildersApi {
99 BuildersApi {
100 http_client: self.http_client.clone(),
101 }
102 }
103
104 pub fn leaderboard(&self) -> LeaderboardApi {
106 LeaderboardApi {
107 http_client: self.http_client.clone(),
108 }
109 }
110
111 pub fn market_positions(&self) -> MarketPositionsApi {
113 MarketPositionsApi {
114 http_client: self.http_client.clone(),
115 }
116 }
117
118 pub fn accounting(&self) -> AccountingApi {
120 AccountingApi {
121 http_client: self.http_client.clone(),
122 }
123 }
124
125 pub fn combos(&self) -> CombosApi {
127 CombosApi {
128 http_client: self.http_client.clone(),
129 }
130 }
131
132 pub fn misc(&self) -> MiscApi {
134 MiscApi {
135 http_client: self.http_client.clone(),
136 }
137 }
138}
139
140pub struct DataApiBuilder {
142 base_url: String,
143 timeout_ms: u64,
144 pool_size: usize,
145 retry_config: Option<RetryConfig>,
146 max_concurrent: Option<usize>,
147}
148
149impl DataApiBuilder {
150 fn new() -> Self {
151 Self {
152 base_url: DEFAULT_BASE_URL.to_string(),
153 timeout_ms: DEFAULT_TIMEOUT_MS,
154 pool_size: DEFAULT_POOL_SIZE,
155 retry_config: None,
156 max_concurrent: None,
157 }
158 }
159
160 pub fn base_url(mut self, url: impl Into<String>) -> Self {
162 self.base_url = url.into();
163 self
164 }
165
166 pub fn timeout_ms(mut self, timeout: u64) -> Self {
168 self.timeout_ms = timeout;
169 self
170 }
171
172 pub fn pool_size(mut self, size: usize) -> Self {
174 self.pool_size = size;
175 self
176 }
177
178 pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
180 self.retry_config = Some(config);
181 self
182 }
183
184 pub fn max_concurrent(mut self, max: usize) -> Self {
188 self.max_concurrent = Some(max);
189 self
190 }
191
192 pub fn build(self) -> Result<DataApi, DataApiError> {
194 let mut builder = HttpClientBuilder::new(&self.base_url)
195 .timeout_ms(self.timeout_ms)
196 .pool_size(self.pool_size)
197 .with_rate_limiter(RateLimiter::data_default())
198 .with_max_concurrent(self.max_concurrent.unwrap_or(4));
199 if let Some(config) = self.retry_config {
200 builder = builder.with_retry_config(config);
201 }
202 let http_client = builder.build()?;
203
204 Ok(DataApi { http_client })
205 }
206}
207
208impl Default for DataApiBuilder {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214pub struct Traded {
216 user_api: UserApi,
217}
218
219impl Traded {
220 pub async fn get(self) -> std::result::Result<UserTraded, DataApiError> {
222 self.user_api.traded().await
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_builder_default() {
232 let builder = DataApiBuilder::default();
233 assert_eq!(builder.base_url, DEFAULT_BASE_URL);
234 }
235
236 #[test]
237 fn test_builder_custom_retry_config() {
238 let config = RetryConfig {
239 max_retries: 5,
240 initial_backoff_ms: 1000,
241 max_backoff_ms: 30_000,
242 };
243 let builder = DataApiBuilder::new().with_retry_config(config);
244 let config = builder.retry_config.unwrap();
245 assert_eq!(config.max_retries, 5);
246 assert_eq!(config.initial_backoff_ms, 1000);
247 }
248
249 #[test]
250 fn test_builder_custom_max_concurrent() {
251 let builder = DataApiBuilder::new().max_concurrent(10);
252 assert_eq!(builder.max_concurrent, Some(10));
253 }
254
255 #[tokio::test]
256 async fn test_default_concurrency_limit_is_4() {
257 let data = DataApi::new().unwrap();
258 let mut permits = Vec::new();
259 for _ in 0..4 {
260 permits.push(data.http_client.acquire_concurrency().await);
261 }
262 assert!(permits.iter().all(|p| p.is_some()));
263
264 let result = tokio::time::timeout(
265 std::time::Duration::from_millis(50),
266 data.http_client.acquire_concurrency(),
267 )
268 .await;
269 assert!(
270 result.is_err(),
271 "5th permit should block with default limit of 4"
272 );
273 }
274
275 #[test]
276 fn test_builder_build_success() {
277 let data = DataApi::builder().build();
278 assert!(data.is_ok());
279 }
280}