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