Skip to main content

polyoxide_data/api/
builders.rs

1use polyoxide_core::{HttpClient, QueryBuilder, Request};
2use serde::{Deserialize, Serialize};
3
4use crate::error::DataApiError;
5
6// Re-export for backwards compatibility
7pub use crate::types::TimePeriod;
8
9/// Builders namespace for builder-related operations
10#[derive(Clone)]
11pub struct BuildersApi {
12    pub(crate) http_client: HttpClient,
13}
14
15impl BuildersApi {
16    /// Get the aggregated builder leaderboard
17    pub fn leaderboard(&self) -> GetBuilderLeaderboard {
18        let request = Request::new(self.http_client.clone(), "/v1/builders/leaderboard");
19
20        GetBuilderLeaderboard { request }
21    }
22
23    /// Get daily builder volume time series
24    pub fn volume(&self) -> GetBuilderVolume {
25        let request = Request::new(self.http_client.clone(), "/v1/builders/volume");
26
27        GetBuilderVolume { request }
28    }
29}
30
31/// Request builder for getting the builder leaderboard
32pub struct GetBuilderLeaderboard {
33    request: Request<Vec<BuilderRanking>, DataApiError>,
34}
35
36impl GetBuilderLeaderboard {
37    /// Set the aggregation time period (default: DAY)
38    pub fn time_period(mut self, period: TimePeriod) -> Self {
39        self.request = self.request.query("timePeriod", period);
40        self
41    }
42
43    /// Set maximum number of results (0-50, default: 25)
44    pub fn limit(mut self, limit: u32) -> Self {
45        self.request = self.request.query("limit", limit);
46        self
47    }
48
49    /// Set pagination offset (0-1000, default: 0)
50    pub fn offset(mut self, offset: u32) -> Self {
51        self.request = self.request.query("offset", offset);
52        self
53    }
54
55    /// Execute the request
56    pub async fn send(self) -> Result<Vec<BuilderRanking>, DataApiError> {
57        self.request.send().await
58    }
59}
60
61/// Builder ranking entry in the leaderboard
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(rename_all(deserialize = "camelCase"))]
64pub struct BuilderRanking {
65    /// Builder's ranking position
66    pub rank: String,
67    /// Builder identifier/name
68    pub builder: String,
69    /// Trading volume metric
70    pub volume: f64,
71    /// Count of active users
72    pub active_users: u64,
73    /// Verification status
74    pub verified: bool,
75    /// Logo image URL
76    pub builder_logo: Option<String>,
77}
78
79/// Request builder for getting the builder volume time series
80pub struct GetBuilderVolume {
81    request: Request<Vec<BuilderVolume>, DataApiError>,
82}
83
84impl GetBuilderVolume {
85    /// Set the time period filter (default: DAY)
86    pub fn time_period(mut self, period: TimePeriod) -> Self {
87        self.request = self.request.query("timePeriod", period);
88        self
89    }
90
91    /// Execute the request
92    pub async fn send(self) -> Result<Vec<BuilderVolume>, DataApiError> {
93        self.request.send().await
94    }
95}
96
97/// Builder volume entry in the time series
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(rename_all(deserialize = "camelCase"))]
100pub struct BuilderVolume {
101    /// Date/time of the volume record (ISO 8601)
102    pub dt: String,
103    /// Builder identifier/name
104    pub builder: String,
105    /// Logo image URL
106    pub builder_logo: Option<String>,
107    /// Verification status
108    pub verified: bool,
109    /// Trading volume metric
110    pub volume: f64,
111    /// Count of active users
112    pub active_users: u64,
113    /// Builder's ranking position
114    pub rank: String,
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn time_period_display_matches_serde() {
123        let variants = [
124            TimePeriod::Day,
125            TimePeriod::Week,
126            TimePeriod::Month,
127            TimePeriod::All,
128        ];
129        for variant in variants {
130            let serialized = serde_json::to_value(variant).unwrap();
131            let display = variant.to_string();
132            assert_eq!(
133                format!("\"{}\"", display),
134                serialized.to_string(),
135                "Display mismatch for {:?}",
136                variant
137            );
138        }
139    }
140
141    #[test]
142    fn time_period_serde_roundtrip() {
143        for variant in [
144            TimePeriod::Day,
145            TimePeriod::Week,
146            TimePeriod::Month,
147            TimePeriod::All,
148        ] {
149            let json = serde_json::to_string(&variant).unwrap();
150            let deserialized: TimePeriod = serde_json::from_str(&json).unwrap();
151            assert_eq!(variant, deserialized);
152        }
153    }
154
155    #[test]
156    fn time_period_default_is_day() {
157        assert_eq!(TimePeriod::default(), TimePeriod::Day);
158    }
159
160    #[test]
161    fn time_period_specific_values() {
162        assert_eq!(TimePeriod::Day.to_string(), "DAY");
163        assert_eq!(TimePeriod::Week.to_string(), "WEEK");
164        assert_eq!(TimePeriod::Month.to_string(), "MONTH");
165        assert_eq!(TimePeriod::All.to_string(), "ALL");
166    }
167
168    #[test]
169    fn deserialize_builder_ranking() {
170        let json = r#"{
171            "rank": "1",
172            "builder": "polymarket-app",
173            "volume": 1500000.50,
174            "activeUsers": 25000,
175            "verified": true,
176            "builderLogo": "https://example.com/logo.png"
177        }"#;
178
179        let ranking: BuilderRanking = serde_json::from_str(json).unwrap();
180        assert_eq!(ranking.rank, "1");
181        assert_eq!(ranking.builder, "polymarket-app");
182        assert!((ranking.volume - 1500000.50).abs() < f64::EPSILON);
183        assert_eq!(ranking.active_users, 25000);
184        assert!(ranking.verified);
185        assert_eq!(
186            ranking.builder_logo,
187            Some("https://example.com/logo.png".to_string())
188        );
189    }
190
191    #[test]
192    fn deserialize_builder_ranking_null_logo() {
193        let json = r#"{
194            "rank": "5",
195            "builder": "unknown-builder",
196            "volume": 100.0,
197            "activeUsers": 10,
198            "verified": false,
199            "builderLogo": null
200        }"#;
201
202        let ranking: BuilderRanking = serde_json::from_str(json).unwrap();
203        assert_eq!(ranking.rank, "5");
204        assert!(!ranking.verified);
205        assert!(ranking.builder_logo.is_none());
206    }
207
208    #[test]
209    fn deserialize_builder_volume() {
210        let json = r#"{
211            "dt": "2025-01-15T00:00:00Z",
212            "builder": "top-builder",
213            "builderLogo": null,
214            "verified": true,
215            "volume": 500000.0,
216            "activeUsers": 1200,
217            "rank": "3"
218        }"#;
219
220        let vol: BuilderVolume = serde_json::from_str(json).unwrap();
221        assert_eq!(vol.dt, "2025-01-15T00:00:00Z");
222        assert_eq!(vol.builder, "top-builder");
223        assert!(vol.verified);
224        assert!((vol.volume - 500000.0).abs() < f64::EPSILON);
225        assert_eq!(vol.active_users, 1200);
226        assert_eq!(vol.rank, "3");
227        assert!(vol.builder_logo.is_none());
228    }
229
230    #[test]
231    fn deserialize_builder_ranking_list() {
232        let json = r#"[
233            {"rank": "1", "builder": "a", "volume": 100.0, "activeUsers": 5, "verified": true, "builderLogo": null},
234            {"rank": "2", "builder": "b", "volume": 50.0, "activeUsers": 3, "verified": false, "builderLogo": null}
235        ]"#;
236
237        let rankings: Vec<BuilderRanking> = serde_json::from_str(json).unwrap();
238        assert_eq!(rankings.len(), 2);
239        assert_eq!(rankings[0].rank, "1");
240        assert_eq!(rankings[1].rank, "2");
241    }
242}