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}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn deserialize_market_holders() {
91        let json = r#"{
92            "token": "token_abc",
93            "holders": [
94                {
95                    "proxyWallet": "0xholder1",
96                    "bio": "Top trader",
97                    "asset": "token_abc",
98                    "pseudonym": "whale1",
99                    "amount": 50000.0,
100                    "displayUsernamePublic": true,
101                    "outcomeIndex": 0,
102                    "name": "Holder One",
103                    "profileImage": "https://example.com/img.png",
104                    "profileImageOptimized": "https://example.com/img_opt.png"
105                },
106                {
107                    "proxyWallet": "0xholder2",
108                    "bio": null,
109                    "asset": null,
110                    "pseudonym": null,
111                    "amount": 1000.0,
112                    "displayUsernamePublic": null,
113                    "outcomeIndex": 1,
114                    "name": null,
115                    "profileImage": null,
116                    "profileImageOptimized": null
117                }
118            ]
119        }"#;
120
121        let mh: MarketHolders = serde_json::from_str(json).unwrap();
122        assert_eq!(mh.token, "token_abc");
123        assert_eq!(mh.holders.len(), 2);
124
125        let h1 = &mh.holders[0];
126        assert_eq!(h1.proxy_wallet, "0xholder1");
127        assert_eq!(h1.bio, Some("Top trader".to_string()));
128        assert!((h1.amount - 50000.0).abs() < f64::EPSILON);
129        assert_eq!(h1.outcome_index, 0);
130        assert_eq!(h1.display_username_public, Some(true));
131        assert_eq!(h1.name, Some("Holder One".to_string()));
132
133        let h2 = &mh.holders[1];
134        assert_eq!(h2.proxy_wallet, "0xholder2");
135        assert!(h2.bio.is_none());
136        assert!(h2.asset.is_none());
137        assert!(h2.pseudonym.is_none());
138        assert!((h2.amount - 1000.0).abs() < f64::EPSILON);
139        assert_eq!(h2.outcome_index, 1);
140        assert!(h2.name.is_none());
141    }
142
143    #[test]
144    fn deserialize_empty_holders_list() {
145        let json = r#"{"token": "empty_token", "holders": []}"#;
146        let mh: MarketHolders = serde_json::from_str(json).unwrap();
147        assert_eq!(mh.token, "empty_token");
148        assert!(mh.holders.is_empty());
149    }
150}