Skip to main content

blueprint_eigenlayer_extra/sidecar/
client.rs

1use super::types::*;
2use crate::error::{EigenlayerExtraError, Result};
3use reqwest::Client;
4use std::time::Duration;
5
6/// Sidecar API client for EigenLayer rewards data.
7///
8/// Communicates with EigenLayer's Sidecar API to fetch rewards proofs and summaries.
9/// See: https://github.com/Layr-Labs/sidecar
10#[derive(Clone)]
11pub struct SidecarClient {
12    base_url: String,
13    client: Client,
14}
15
16impl SidecarClient {
17    /// Create a new Sidecar client
18    ///
19    /// # Arguments
20    /// * `base_url` - Base URL for the Sidecar API (e.g., https://sidecar-rpc.eigenlayer.xyz/mainnet)
21    pub fn new(base_url: String) -> Result<Self> {
22        let client = Client::builder()
23            .timeout(Duration::from_secs(300))
24            .build()
25            .map_err(|e| {
26                EigenlayerExtraError::Other(format!("Failed to create HTTP client: {e}"))
27            })?;
28
29        Ok(Self { base_url, client })
30    }
31
32    /// Generate a claim proof for the given earner and tokens
33    ///
34    /// # Arguments
35    /// * `earner_address` - Address of the earner
36    /// * `tokens` - List of token addresses to claim (empty vec claims all tokens)
37    /// * `root_index` - Optional root index (uses current active root if None)
38    pub async fn generate_claim_proof(
39        &self,
40        earner_address: &str,
41        tokens: Vec<String>,
42        root_index: Option<i64>,
43    ) -> Result<Proof> {
44        let url = format!("{}/rewards/v1/claim-proof", self.base_url);
45        let req = GenerateClaimProofRequest {
46            earner_address: earner_address.to_string(),
47            tokens,
48            root_index,
49        };
50
51        let response = self
52            .client
53            .post(&url)
54            .header("x-sidecar-source", "tangle-cli")
55            .json(&req)
56            .send()
57            .await
58            .map_err(|e| EigenlayerExtraError::Other(format!("Sidecar request failed: {e}")))?;
59
60        if !response.status().is_success() {
61            let status = response.status();
62            let body = response.text().await.unwrap_or_default();
63            return Err(EigenlayerExtraError::Other(format!(
64                "Sidecar API error {status}: {body}"
65            )));
66        }
67
68        let resp: GenerateClaimProofResponse = response
69            .json()
70            .await
71            .map_err(|e| EigenlayerExtraError::Other(format!("Failed to parse response: {e}")))?;
72
73        Ok(resp.proof)
74    }
75
76    /// Get summarized rewards for an earner
77    ///
78    /// Returns earned, active, claimed, and claimable amounts per token
79    ///
80    /// # Arguments
81    /// * `earner_address` - Address of the earner
82    /// * `block_height` - Optional block height (uses latest if None)
83    pub async fn get_summarized_rewards(
84        &self,
85        earner_address: &str,
86        block_height: Option<u64>,
87    ) -> Result<Vec<SummarizedEarnerReward>> {
88        let mut url = format!(
89            "{}/rewards/v1/earners/{}/summarized-rewards",
90            self.base_url, earner_address
91        );
92
93        if let Some(height) = block_height {
94            url.push_str(&format!("?blockHeight={height}"));
95        }
96
97        let response = self
98            .client
99            .get(&url)
100            .header("x-sidecar-source", "tangle-cli")
101            .send()
102            .await
103            .map_err(|e| EigenlayerExtraError::Other(format!("Sidecar request failed: {e}")))?;
104
105        if !response.status().is_success() {
106            let status = response.status();
107            let body = response.text().await.unwrap_or_default();
108            return Err(EigenlayerExtraError::Other(format!(
109                "Sidecar API error {status}: {body}"
110            )));
111        }
112
113        let resp: GetSummarizedRewardsResponse = response
114            .json()
115            .await
116            .map_err(|e| EigenlayerExtraError::Other(format!("Failed to parse response: {e}")))?;
117
118        Ok(resp.rewards)
119    }
120
121    /// List distribution roots
122    ///
123    /// # Arguments
124    /// * `block_height` - Optional block height filter
125    pub async fn list_distribution_roots(
126        &self,
127        block_height: Option<u64>,
128    ) -> Result<Vec<DistributionRoot>> {
129        let mut url = format!("{}/rewards/v1/distribution-roots", self.base_url);
130
131        if let Some(height) = block_height {
132            url.push_str(&format!("?blockHeight={height}"));
133        }
134
135        let response = self
136            .client
137            .get(&url)
138            .header("x-sidecar-source", "tangle-cli")
139            .send()
140            .await
141            .map_err(|e| EigenlayerExtraError::Other(format!("Sidecar request failed: {e}")))?;
142
143        if !response.status().is_success() {
144            let status = response.status();
145            let body = response.text().await.unwrap_or_default();
146            return Err(EigenlayerExtraError::Other(format!(
147                "Sidecar API error {status}: {body}"
148            )));
149        }
150
151        let resp: ListDistributionRootsResponse = response
152            .json()
153            .await
154            .map_err(|e| EigenlayerExtraError::Other(format!("Failed to parse response: {e}")))?;
155
156        Ok(resp.distribution_roots)
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[tokio::test]
165    #[ignore = "Requires Sidecar API endpoint"]
166    async fn test_sidecar_client_creation() {
167        let client = SidecarClient::new("https://sidecar-rpc.eigenlayer.xyz/holesky".to_string());
168        assert!(client.is_ok());
169    }
170
171    #[tokio::test]
172    #[ignore = "Requires Sidecar API endpoint and valid earner address"]
173    async fn test_get_summarized_rewards() {
174        let client =
175            SidecarClient::new("https://sidecar-rpc.eigenlayer.xyz/holesky".to_string()).unwrap();
176
177        // Use a test address - replace with actual earner address for real testing
178        let result = client
179            .get_summarized_rewards("0x0000000000000000000000000000000000000000", None)
180            .await;
181
182        // This test will fail with invalid address, but verifies the client works
183        println!("Result: {result:?}");
184    }
185}