Skip to main content

polyoxide_data/api/
holders.rs

1use polyoxide_core::{HttpClient, QueryBuilder, Request};
2use serde::{Deserialize, Serialize};
3
4use crate::error::DataApiError;
5
6/// Holders namespace for holder-related operations
7#[derive(Clone)]
8pub struct Holders {
9    pub(crate) http_client: HttpClient,
10}
11
12impl Holders {
13    /// Get top holders for markets
14    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
25/// Request builder for getting top holders
26pub struct ListHolders {
27    request: Request<Vec<MarketHolders>, DataApiError>,
28}
29
30impl ListHolders {
31    /// Set maximum number of results per market (0-500, default: 100)
32    pub fn limit(mut self, limit: u32) -> Self {
33        self.request = self.request.query("limit", limit);
34        self
35    }
36
37    /// Set minimum balance filter (0-999999, default: 1)
38    pub fn min_balance(mut self, min_balance: u32) -> Self {
39        self.request = self.request.query("minBalance", min_balance);
40        self
41    }
42
43    /// Execute the request
44    pub async fn send(self) -> Result<Vec<MarketHolders>, DataApiError> {
45        self.request.send().await
46    }
47}
48
49/// Market holders response containing token and its holders
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(rename_all(deserialize = "camelCase"))]
52pub struct MarketHolders {
53    /// Token identifier
54    pub token: String,
55    /// List of holders for this token
56    pub holders: Vec<Holder>,
57}
58
59/// Individual holder of a market token
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(rename_all(deserialize = "camelCase"))]
62pub struct Holder {
63    /// Proxy wallet address
64    pub proxy_wallet: String,
65    /// User bio
66    pub bio: Option<String>,
67    /// Asset identifier (token ID)
68    pub asset: Option<String>,
69    /// User pseudonym
70    pub pseudonym: Option<String>,
71    /// Amount held
72    pub amount: f64,
73    /// Whether username is displayed publicly
74    pub display_username_public: Option<bool>,
75    /// Outcome index (0 or 1 for binary markets)
76    pub outcome_index: u32,
77    /// User display name
78    pub name: Option<String>,
79    /// User profile image URL
80    pub profile_image: Option<String>,
81    /// Optimized profile image URL
82    pub profile_image_optimized: Option<String>,
83    /// Whether the user is verified
84    #[serde(default)]
85    pub verified: Option<bool>,
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn deserialize_market_holders() {
94        let json = r#"{
95            "token": "token_abc",
96            "holders": [
97                {
98                    "proxyWallet": "0xholder1",
99                    "bio": "Top trader",
100                    "asset": "token_abc",
101                    "pseudonym": "whale1",
102                    "amount": 50000.0,
103                    "displayUsernamePublic": true,
104                    "outcomeIndex": 0,
105                    "name": "Holder One",
106                    "profileImage": "https://example.com/img.png",
107                    "profileImageOptimized": "https://example.com/img_opt.png",
108                    "verified": true
109                },
110                {
111                    "proxyWallet": "0xholder2",
112                    "bio": null,
113                    "asset": null,
114                    "pseudonym": null,
115                    "amount": 1000.0,
116                    "displayUsernamePublic": null,
117                    "outcomeIndex": 1,
118                    "name": null,
119                    "profileImage": null,
120                    "profileImageOptimized": null,
121                    "verified": false
122                }
123            ]
124        }"#;
125
126        let mh: MarketHolders = serde_json::from_str(json).unwrap();
127        assert_eq!(mh.token, "token_abc");
128        assert_eq!(mh.holders.len(), 2);
129
130        let h1 = &mh.holders[0];
131        assert_eq!(h1.proxy_wallet, "0xholder1");
132        assert_eq!(h1.bio, Some("Top trader".to_string()));
133        assert!((h1.amount - 50000.0).abs() < f64::EPSILON);
134        assert_eq!(h1.outcome_index, 0);
135        assert_eq!(h1.display_username_public, Some(true));
136        assert_eq!(h1.name, Some("Holder One".to_string()));
137        assert_eq!(h1.verified, Some(true));
138
139        let h2 = &mh.holders[1];
140        assert_eq!(h2.proxy_wallet, "0xholder2");
141        assert!(h2.bio.is_none());
142        assert!(h2.asset.is_none());
143        assert!(h2.pseudonym.is_none());
144        assert!((h2.amount - 1000.0).abs() < f64::EPSILON);
145        assert_eq!(h2.outcome_index, 1);
146        assert!(h2.name.is_none());
147        assert_eq!(h2.verified, Some(false));
148    }
149
150    #[test]
151    fn deserialize_empty_holders_list() {
152        let json = r#"{"token": "empty_token", "holders": []}"#;
153        let mh: MarketHolders = serde_json::from_str(json).unwrap();
154        assert_eq!(mh.token, "empty_token");
155        assert!(mh.holders.is_empty());
156    }
157
158    #[test]
159    fn holder_without_verified_field() {
160        let json = r#"{
161            "proxyWallet": "0xholder",
162            "amount": 100.0,
163            "outcomeIndex": 0
164        }"#;
165        let h: Holder = serde_json::from_str(json).unwrap();
166        assert_eq!(h.proxy_wallet, "0xholder");
167        assert!(h.verified.is_none());
168    }
169}