1use polyoxide_core::{
2 HttpClient, HttpClientBuilder, RateLimiter, RetryConfig, DEFAULT_POOL_SIZE, DEFAULT_TIMEOUT_MS,
3};
4
5use crate::{
6 api::{
7 builders::BuildersApi,
8 health::Health,
9 holders::Holders,
10 leaderboard::LeaderboardApi,
11 live_volume::LiveVolumeApi,
12 open_interest::OpenInterestApi,
13 trades::Trades,
14 users::{UserApi, UserTraded},
15 },
16 error::DataApiError,
17};
18
19const DEFAULT_BASE_URL: &str = "https://data-api.polymarket.com";
20
21#[derive(Clone)]
23pub struct DataApi {
24 pub(crate) http_client: HttpClient,
25}
26
27impl DataApi {
28 pub fn new() -> Result<Self, DataApiError> {
30 Self::builder().build()
31 }
32
33 pub fn builder() -> DataApiBuilder {
35 DataApiBuilder::new()
36 }
37
38 pub fn health(&self) -> Health {
40 Health {
41 http_client: self.http_client.clone(),
42 }
43 }
44
45 pub fn user(&self, user_address: impl Into<String>) -> UserApi {
47 UserApi {
48 http_client: self.http_client.clone(),
49 user_address: user_address.into(),
50 }
51 }
52
53 pub fn positions(&self, user_address: impl Into<String>) -> UserApi {
55 self.user(user_address)
56 }
57
58 pub fn traded(&self, user_address: impl Into<String>) -> Traded {
60 Traded {
61 user_api: self.user(user_address),
62 }
63 }
64
65 pub fn trades(&self) -> Trades {
67 Trades {
68 http_client: self.http_client.clone(),
69 }
70 }
71
72 pub fn holders(&self) -> Holders {
74 Holders {
75 http_client: self.http_client.clone(),
76 }
77 }
78
79 pub fn open_interest(&self) -> OpenInterestApi {
81 OpenInterestApi {
82 http_client: self.http_client.clone(),
83 }
84 }
85
86 pub fn live_volume(&self) -> LiveVolumeApi {
88 LiveVolumeApi {
89 http_client: self.http_client.clone(),
90 }
91 }
92
93 pub fn builders(&self) -> BuildersApi {
95 BuildersApi {
96 http_client: self.http_client.clone(),
97 }
98 }
99
100 pub fn leaderboard(&self) -> LeaderboardApi {
102 LeaderboardApi {
103 http_client: self.http_client.clone(),
104 }
105 }
106}
107
108pub struct DataApiBuilder {
110 base_url: String,
111 timeout_ms: u64,
112 pool_size: usize,
113 retry_config: Option<RetryConfig>,
114 max_concurrent: Option<usize>,
115}
116
117impl DataApiBuilder {
118 fn new() -> Self {
119 Self {
120 base_url: DEFAULT_BASE_URL.to_string(),
121 timeout_ms: DEFAULT_TIMEOUT_MS,
122 pool_size: DEFAULT_POOL_SIZE,
123 retry_config: None,
124 max_concurrent: None,
125 }
126 }
127
128 pub fn base_url(mut self, url: impl Into<String>) -> Self {
130 self.base_url = url.into();
131 self
132 }
133
134 pub fn timeout_ms(mut self, timeout: u64) -> Self {
136 self.timeout_ms = timeout;
137 self
138 }
139
140 pub fn pool_size(mut self, size: usize) -> Self {
142 self.pool_size = size;
143 self
144 }
145
146 pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
148 self.retry_config = Some(config);
149 self
150 }
151
152 pub fn max_concurrent(mut self, max: usize) -> Self {
156 self.max_concurrent = Some(max);
157 self
158 }
159
160 pub fn build(self) -> Result<DataApi, DataApiError> {
162 let mut builder = HttpClientBuilder::new(&self.base_url)
163 .timeout_ms(self.timeout_ms)
164 .pool_size(self.pool_size)
165 .with_rate_limiter(RateLimiter::data_default())
166 .with_max_concurrent(self.max_concurrent.unwrap_or(4));
167 if let Some(config) = self.retry_config {
168 builder = builder.with_retry_config(config);
169 }
170 let http_client = builder.build()?;
171
172 Ok(DataApi { http_client })
173 }
174}
175
176impl Default for DataApiBuilder {
177 fn default() -> Self {
178 Self::new()
179 }
180}
181
182pub struct Traded {
184 user_api: UserApi,
185}
186
187impl Traded {
188 pub async fn get(self) -> std::result::Result<UserTraded, DataApiError> {
190 self.user_api.traded().await
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_builder_default() {
200 let builder = DataApiBuilder::default();
201 assert_eq!(builder.base_url, DEFAULT_BASE_URL);
202 }
203
204 #[test]
205 fn test_builder_custom_retry_config() {
206 let config = RetryConfig {
207 max_retries: 5,
208 initial_backoff_ms: 1000,
209 max_backoff_ms: 30_000,
210 };
211 let builder = DataApiBuilder::new().with_retry_config(config);
212 let config = builder.retry_config.unwrap();
213 assert_eq!(config.max_retries, 5);
214 assert_eq!(config.initial_backoff_ms, 1000);
215 }
216
217 #[test]
218 fn test_builder_custom_max_concurrent() {
219 let builder = DataApiBuilder::new().max_concurrent(10);
220 assert_eq!(builder.max_concurrent, Some(10));
221 }
222
223 #[tokio::test]
224 async fn test_default_concurrency_limit_is_4() {
225 let data = DataApi::new().unwrap();
226 let mut permits = Vec::new();
227 for _ in 0..4 {
228 permits.push(data.http_client.acquire_concurrency().await);
229 }
230 assert!(permits.iter().all(|p| p.is_some()));
231
232 let result = tokio::time::timeout(
233 std::time::Duration::from_millis(50),
234 data.http_client.acquire_concurrency(),
235 )
236 .await;
237 assert!(
238 result.is_err(),
239 "5th permit should block with default limit of 4"
240 );
241 }
242
243 #[test]
244 fn test_builder_build_success() {
245 let data = DataApi::builder().build();
246 assert!(data.is_ok());
247 }
248}