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