1use polyoxide_core::{HttpClient, QueryBuilder, Request};
2use serde::{Deserialize, Serialize};
3
4use crate::{error::DataApiError, types::TimePeriod};
5
6#[derive(Clone)]
8pub struct LeaderboardApi {
9 pub(crate) http_client: HttpClient,
10}
11
12impl LeaderboardApi {
13 pub fn get(&self) -> GetLeaderboard {
15 let request = Request::new(self.http_client.clone(), "/v1/leaderboard");
16 GetLeaderboard { request }
17 }
18}
19
20pub struct GetLeaderboard {
22 request: Request<Vec<TraderRanking>, DataApiError>,
23}
24
25impl GetLeaderboard {
26 pub fn category(mut self, category: LeaderboardCategory) -> Self {
28 self.request = self.request.query("category", category);
29 self
30 }
31
32 pub fn time_period(mut self, period: TimePeriod) -> Self {
34 self.request = self.request.query("timePeriod", period);
35 self
36 }
37
38 pub fn order_by(mut self, order_by: LeaderboardOrderBy) -> Self {
40 self.request = self.request.query("orderBy", order_by);
41 self
42 }
43
44 pub fn limit(mut self, limit: u32) -> Self {
46 self.request = self.request.query("limit", limit);
47 self
48 }
49
50 pub fn offset(mut self, offset: u32) -> Self {
52 self.request = self.request.query("offset", offset);
53 self
54 }
55
56 pub fn user(mut self, address: impl Into<String>) -> Self {
58 self.request = self.request.query("user", address.into());
59 self
60 }
61
62 pub fn user_name(mut self, name: impl Into<String>) -> Self {
64 self.request = self.request.query("userName", name.into());
65 self
66 }
67
68 pub async fn send(self) -> Result<Vec<TraderRanking>, DataApiError> {
70 self.request.send().await
71 }
72}
73
74#[cfg_attr(feature = "specta", derive(specta::Type))]
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
77#[serde(rename_all = "UPPERCASE")]
78pub enum LeaderboardCategory {
79 #[default]
81 Overall,
82 Politics,
84 Sports,
86 Crypto,
88 Culture,
90 Mentions,
92 Weather,
94 Economics,
96 Tech,
98 Finance,
100}
101
102impl std::fmt::Display for LeaderboardCategory {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 match self {
105 Self::Overall => write!(f, "OVERALL"),
106 Self::Politics => write!(f, "POLITICS"),
107 Self::Sports => write!(f, "SPORTS"),
108 Self::Crypto => write!(f, "CRYPTO"),
109 Self::Culture => write!(f, "CULTURE"),
110 Self::Mentions => write!(f, "MENTIONS"),
111 Self::Weather => write!(f, "WEATHER"),
112 Self::Economics => write!(f, "ECONOMICS"),
113 Self::Tech => write!(f, "TECH"),
114 Self::Finance => write!(f, "FINANCE"),
115 }
116 }
117}
118
119#[cfg_attr(feature = "specta", derive(specta::Type))]
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
122#[serde(rename_all = "UPPERCASE")]
123pub enum LeaderboardOrderBy {
124 #[default]
126 Pnl,
127 Vol,
129}
130
131impl std::fmt::Display for LeaderboardOrderBy {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 match self {
134 Self::Pnl => write!(f, "PNL"),
135 Self::Vol => write!(f, "VOL"),
136 }
137 }
138}
139
140#[cfg_attr(feature = "specta", derive(specta::Type))]
142#[derive(Debug, Clone, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct TraderRanking {
145 pub rank: String,
147 pub proxy_wallet: String,
149 pub user_name: Option<String>,
151 pub vol: f64,
153 pub pnl: f64,
155 pub profile_image: Option<String>,
157 pub x_username: Option<String>,
159 pub verified_badge: Option<bool>,
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::DataApi;
167
168 fn client() -> DataApi {
169 DataApi::new().unwrap()
170 }
171
172 #[test]
173 fn test_get_leaderboard_full_chain() {
174 let _builder = client()
175 .leaderboard()
176 .get()
177 .category(LeaderboardCategory::Politics)
178 .time_period(TimePeriod::Week)
179 .order_by(LeaderboardOrderBy::Vol)
180 .limit(25)
181 .offset(0)
182 .user("0x1234")
183 .user_name("trader");
184 }
185
186 #[test]
187 fn leaderboard_category_display_matches_serde() {
188 let variants = [
189 LeaderboardCategory::Overall,
190 LeaderboardCategory::Politics,
191 LeaderboardCategory::Sports,
192 LeaderboardCategory::Crypto,
193 LeaderboardCategory::Culture,
194 LeaderboardCategory::Mentions,
195 LeaderboardCategory::Weather,
196 LeaderboardCategory::Economics,
197 LeaderboardCategory::Tech,
198 LeaderboardCategory::Finance,
199 ];
200 for variant in variants {
201 let serialized = serde_json::to_value(variant).unwrap();
202 let display = variant.to_string();
203 assert_eq!(
204 format!("\"{}\"", display),
205 serialized.to_string(),
206 "Display mismatch for {:?}",
207 variant
208 );
209 }
210 }
211
212 #[test]
213 fn leaderboard_order_by_display_matches_serde() {
214 let variants = [LeaderboardOrderBy::Pnl, LeaderboardOrderBy::Vol];
215 for variant in variants {
216 let serialized = serde_json::to_value(variant).unwrap();
217 let display = variant.to_string();
218 assert_eq!(
219 format!("\"{}\"", display),
220 serialized.to_string(),
221 "Display mismatch for {:?}",
222 variant
223 );
224 }
225 }
226
227 #[test]
228 fn leaderboard_category_default_is_overall() {
229 assert_eq!(LeaderboardCategory::default(), LeaderboardCategory::Overall);
230 }
231
232 #[test]
233 fn leaderboard_order_by_default_is_pnl() {
234 assert_eq!(LeaderboardOrderBy::default(), LeaderboardOrderBy::Pnl);
235 }
236
237 #[test]
238 fn deserialize_trader_ranking() {
239 let json = r#"{
240 "rank": "1",
241 "proxyWallet": "0xabc123",
242 "userName": "top_trader",
243 "vol": 5000000.50,
244 "pnl": 250000.75,
245 "profileImage": "https://example.com/pic.png",
246 "xUsername": "top_trader_x",
247 "verifiedBadge": true
248 }"#;
249 let ranking: TraderRanking = serde_json::from_str(json).unwrap();
250 assert_eq!(ranking.rank, "1");
251 assert_eq!(ranking.proxy_wallet, "0xabc123");
252 assert_eq!(ranking.user_name.as_deref(), Some("top_trader"));
253 assert!((ranking.vol - 5000000.50).abs() < f64::EPSILON);
254 assert!((ranking.pnl - 250000.75).abs() < f64::EPSILON);
255 assert_eq!(ranking.verified_badge, Some(true));
256 }
257
258 #[test]
259 fn deserialize_trader_ranking_minimal() {
260 let json = r#"{
261 "rank": "50",
262 "proxyWallet": "0xdef456",
263 "userName": null,
264 "vol": 100.0,
265 "pnl": -10.0,
266 "profileImage": null,
267 "xUsername": null,
268 "verifiedBadge": null
269 }"#;
270 let ranking: TraderRanking = serde_json::from_str(json).unwrap();
271 assert_eq!(ranking.rank, "50");
272 assert!(ranking.user_name.is_none());
273 assert!(ranking.profile_image.is_none());
274 assert!((ranking.pnl - (-10.0)).abs() < f64::EPSILON);
275 }
276
277 #[test]
278 fn deserialize_trader_ranking_list() {
279 let json = r#"[
280 {"rank": "1", "proxyWallet": "0xa", "userName": null, "vol": 100.0, "pnl": 50.0, "profileImage": null, "xUsername": null, "verifiedBadge": null},
281 {"rank": "2", "proxyWallet": "0xb", "userName": null, "vol": 80.0, "pnl": 30.0, "profileImage": null, "xUsername": null, "verifiedBadge": null}
282 ]"#;
283 let rankings: Vec<TraderRanking> = serde_json::from_str(json).unwrap();
284 assert_eq!(rankings.len(), 2);
285 assert_eq!(rankings[0].rank, "1");
286 assert_eq!(rankings[1].rank, "2");
287 }
288}