polyoxide_data/api/
holders.rs1use polyoxide_core::{HttpClient, QueryBuilder, Request};
2use serde::{Deserialize, Serialize};
3
4use crate::error::DataApiError;
5
6#[derive(Clone)]
8pub struct Holders {
9 pub(crate) http_client: HttpClient,
10}
11
12impl Holders {
13 pub fn list(&self, markets: impl IntoIterator<Item = impl ToString>) -> ListHolders {
15 let market_ids: Vec<String> = markets.into_iter().map(|s| s.to_string()).collect();
16 let mut request = Request::new(self.http_client.clone(), "/holders");
17 if !market_ids.is_empty() {
18 request = request.query("market", market_ids.join(","));
19 }
20
21 ListHolders { request }
22 }
23}
24
25pub struct ListHolders {
27 request: Request<Vec<MarketHolders>, DataApiError>,
28}
29
30impl ListHolders {
31 pub fn limit(mut self, limit: u32) -> Self {
48 self.request = self.request.query("limit", limit);
49 self
50 }
51
52 pub fn min_balance(mut self, min_balance: u32) -> Self {
54 self.request = self.request.query("minBalance", min_balance);
55 self
56 }
57
58 pub async fn send(self) -> Result<Vec<MarketHolders>, DataApiError> {
60 self.request.send().await
61 }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(rename_all(deserialize = "camelCase"))]
67pub struct MarketHolders {
68 pub token: String,
70 pub holders: Vec<Holder>,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76#[serde(rename_all(deserialize = "camelCase"))]
77pub struct Holder {
78 pub proxy_wallet: String,
80 pub bio: Option<String>,
82 pub asset: Option<String>,
84 pub pseudonym: Option<String>,
86 pub amount: f64,
88 pub display_username_public: Option<bool>,
90 pub outcome_index: u32,
92 pub name: Option<String>,
94 pub profile_image: Option<String>,
96 pub profile_image_optimized: Option<String>,
98 #[serde(default)]
100 pub verified: Option<bool>,
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn deserialize_market_holders() {
109 let json = r#"{
110 "token": "token_abc",
111 "holders": [
112 {
113 "proxyWallet": "0xholder1",
114 "bio": "Top trader",
115 "asset": "token_abc",
116 "pseudonym": "whale1",
117 "amount": 50000.0,
118 "displayUsernamePublic": true,
119 "outcomeIndex": 0,
120 "name": "Holder One",
121 "profileImage": "https://example.com/img.png",
122 "profileImageOptimized": "https://example.com/img_opt.png",
123 "verified": true
124 },
125 {
126 "proxyWallet": "0xholder2",
127 "bio": null,
128 "asset": null,
129 "pseudonym": null,
130 "amount": 1000.0,
131 "displayUsernamePublic": null,
132 "outcomeIndex": 1,
133 "name": null,
134 "profileImage": null,
135 "profileImageOptimized": null,
136 "verified": false
137 }
138 ]
139 }"#;
140
141 let mh: MarketHolders = serde_json::from_str(json).unwrap();
142 assert_eq!(mh.token, "token_abc");
143 assert_eq!(mh.holders.len(), 2);
144
145 let h1 = &mh.holders[0];
146 assert_eq!(h1.proxy_wallet, "0xholder1");
147 assert_eq!(h1.bio, Some("Top trader".to_string()));
148 assert!((h1.amount - 50000.0).abs() < f64::EPSILON);
149 assert_eq!(h1.outcome_index, 0);
150 assert_eq!(h1.display_username_public, Some(true));
151 assert_eq!(h1.name, Some("Holder One".to_string()));
152 assert_eq!(h1.verified, Some(true));
153
154 let h2 = &mh.holders[1];
155 assert_eq!(h2.proxy_wallet, "0xholder2");
156 assert!(h2.bio.is_none());
157 assert!(h2.asset.is_none());
158 assert!(h2.pseudonym.is_none());
159 assert!((h2.amount - 1000.0).abs() < f64::EPSILON);
160 assert_eq!(h2.outcome_index, 1);
161 assert!(h2.name.is_none());
162 assert_eq!(h2.verified, Some(false));
163 }
164
165 #[test]
166 fn deserialize_empty_holders_list() {
167 let json = r#"{"token": "empty_token", "holders": []}"#;
168 let mh: MarketHolders = serde_json::from_str(json).unwrap();
169 assert_eq!(mh.token, "empty_token");
170 assert!(mh.holders.is_empty());
171 }
172
173 #[test]
174 fn holder_without_verified_field() {
175 let json = r#"{
176 "proxyWallet": "0xholder",
177 "amount": 100.0,
178 "outcomeIndex": 0
179 }"#;
180 let h: Holder = serde_json::from_str(json).unwrap();
181 assert_eq!(h.proxy_wallet, "0xholder");
182 assert!(h.verified.is_none());
183 }
184}