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        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/// Main Data API client
26#[derive(Clone)]
27pub struct DataApi {
28    pub(crate) http_client: HttpClient,
29}
30
31impl DataApi {
32    /// Create a new Data API client with default configuration
33    pub fn new() -> Result<Self, DataApiError> {
34        Self::builder().build()
35    }
36
37    /// Create a builder for configuring the client
38    pub fn builder() -> DataApiBuilder {
39        DataApiBuilder::new()
40    }
41
42    /// Get health namespace
43    pub fn health(&self) -> Health {
44        Health {
45            http_client: self.http_client.clone(),
46        }
47    }
48
49    /// Get user namespace for user-specific operations
50    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    /// Alias for `user()` - for backwards compatibility
58    pub fn positions(&self, user_address: impl Into<String>) -> UserApi {
59        self.user(user_address)
60    }
61
62    /// Get traded namespace for backwards compatibility
63    pub fn traded(&self, user_address: impl Into<String>) -> Traded {
64        Traded {
65            user_api: self.user(user_address),
66        }
67    }
68
69    /// Get trades namespace
70    pub fn trades(&self) -> Trades {
71        Trades {
72            http_client: self.http_client.clone(),
73        }
74    }
75
76    /// Get holders namespace
77    pub fn holders(&self) -> Holders {
78        Holders {
79            http_client: self.http_client.clone(),
80        }
81    }
82
83    /// Get open interest namespace
84    pub fn open_interest(&self) -> OpenInterestApi {
85        OpenInterestApi {
86            http_client: self.http_client.clone(),
87        }
88    }
89
90    /// Get live volume namespace
91    pub fn live_volume(&self) -> LiveVolumeApi {
92        LiveVolumeApi {
93            http_client: self.http_client.clone(),
94        }
95    }
96
97    /// Get builders namespace
98    pub fn builders(&self) -> BuildersApi {
99        BuildersApi {
100            http_client: self.http_client.clone(),
101        }
102    }
103
104    /// Get leaderboard namespace
105    pub fn leaderboard(&self) -> LeaderboardApi {
106        LeaderboardApi {
107            http_client: self.http_client.clone(),
108        }
109    }
110
111    /// Get market-positions namespace (`/v1/market-positions`)
112    pub fn market_positions(&self) -> MarketPositionsApi {
113        MarketPositionsApi {
114            http_client: self.http_client.clone(),
115        }
116    }
117
118    /// Get accounting namespace (`/v1/accounting/snapshot`, returns ZIP bytes)
119    pub fn accounting(&self) -> AccountingApi {
120        AccountingApi {
121            http_client: self.http_client.clone(),
122        }
123    }
124
125    /// Get combos namespace (`/v1/positions/combos`, `/v1/activity/combos`)
126    pub fn combos(&self) -> CombosApi {
127        CombosApi {
128            http_client: self.http_client.clone(),
129        }
130    }
131
132    /// Get misc namespace (`/other`, `/revisions`)
133    pub fn misc(&self) -> MiscApi {
134        MiscApi {
135            http_client: self.http_client.clone(),
136        }
137    }
138}
139
140/// Builder for configuring Data API client
141pub 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    /// Set base URL for the API
161    pub fn base_url(mut self, url: impl Into<String>) -> Self {
162        self.base_url = url.into();
163        self
164    }
165
166    /// Set request timeout in milliseconds
167    pub fn timeout_ms(mut self, timeout: u64) -> Self {
168        self.timeout_ms = timeout;
169        self
170    }
171
172    /// Set connection pool size
173    pub fn pool_size(mut self, size: usize) -> Self {
174        self.pool_size = size;
175        self
176    }
177
178    /// Set retry configuration for 429 responses
179    pub fn with_retry_config(mut self, config: RetryConfig) -> Self {
180        self.retry_config = Some(config);
181        self
182    }
183
184    /// Set the maximum number of concurrent in-flight requests.
185    ///
186    /// Default: 4. Prevents Cloudflare 1015 errors from request bursts.
187    pub fn max_concurrent(mut self, max: usize) -> Self {
188        self.max_concurrent = Some(max);
189        self
190    }
191
192    /// Build the Data API client
193    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
214/// Wrapper for backwards compatibility with traded() API
215pub struct Traded {
216    user_api: UserApi,
217}
218
219impl Traded {
220    /// Get total markets traded by the user
221    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}