Skip to main content

polyoxide_data/
client.rs

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/// Main Data API client
22#[derive(Clone)]
23pub struct DataApi {
24    pub(crate) http_client: HttpClient,
25}
26
27impl DataApi {
28    /// Create a new Data API client with default configuration
29    pub fn new() -> Result<Self, DataApiError> {
30        Self::builder().build()
31    }
32
33    /// Create a builder for configuring the client
34    pub fn builder() -> DataApiBuilder {
35        DataApiBuilder::new()
36    }
37
38    /// Get health namespace
39    pub fn health(&self) -> Health {
40        Health {
41            http_client: self.http_client.clone(),
42        }
43    }
44
45    /// Get user namespace for user-specific operations
46    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    /// Alias for `user()` - for backwards compatibility
54    pub fn positions(&self, user_address: impl Into<String>) -> UserApi {
55        self.user(user_address)
56    }
57
58    /// Get traded namespace for backwards compatibility
59    pub fn traded(&self, user_address: impl Into<String>) -> Traded {
60        Traded {
61            user_api: self.user(user_address),
62        }
63    }
64
65    /// Get trades namespace
66    pub fn trades(&self) -> Trades {
67        Trades {
68            http_client: self.http_client.clone(),
69        }
70    }
71
72    /// Get holders namespace
73    pub fn holders(&self) -> Holders {
74        Holders {
75            http_client: self.http_client.clone(),
76        }
77    }
78
79    /// Get open interest namespace
80    pub fn open_interest(&self) -> OpenInterestApi {
81        OpenInterestApi {
82            http_client: self.http_client.clone(),
83        }
84    }
85
86    /// Get live volume namespace
87    pub fn live_volume(&self) -> LiveVolumeApi {
88        LiveVolumeApi {
89            http_client: self.http_client.clone(),
90        }
91    }
92
93    /// Get builders namespace
94    pub fn builders(&self) -> BuildersApi {
95        BuildersApi {
96            http_client: self.http_client.clone(),
97        }
98    }
99
100    /// Get leaderboard namespace
101    pub fn leaderboard(&self) -> LeaderboardApi {
102        LeaderboardApi {
103            http_client: self.http_client.clone(),
104        }
105    }
106}
107
108/// Builder for configuring Data API client
109pub 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    /// Set base URL for the API
129    pub fn base_url(mut self, url: impl Into<String>) -> Self {
130        self.base_url = url.into();
131        self
132    }
133
134    /// Set request timeout in milliseconds
135    pub fn timeout_ms(mut self, timeout: u64) -> Self {
136        self.timeout_ms = timeout;
137        self
138    }
139
140    /// Set connection pool size
141    pub fn pool_size(mut self, size: usize) -> Self {
142        self.pool_size = size;
143        self
144    }
145
146    /// Set retry configuration for 429 responses
147    pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
148        self.retry_config = Some(config);
149        self
150    }
151
152    /// Set the maximum number of concurrent in-flight requests.
153    ///
154    /// Default: 4. Prevents Cloudflare 1015 errors from request bursts.
155    pub fn max_concurrent(mut self, max: usize) -> Self {
156        self.max_concurrent = Some(max);
157        self
158    }
159
160    /// Build the Data API client
161    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
182/// Wrapper for backwards compatibility with traded() API
183pub struct Traded {
184    user_api: UserApi,
185}
186
187impl Traded {
188    /// Get total markets traded by the user
189    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}