blueprint_eigenlayer_extra/services/
rewards.rs

1use crate::error::{EigenlayerExtraError, Result};
2use alloy_primitives::{Address, FixedBytes, U256};
3use blueprint_core::info;
4use blueprint_keystore::backends::Backend;
5use blueprint_keystore::backends::eigenlayer::EigenlayerBackend;
6use blueprint_keystore::crypto::k256::K256Ecdsa;
7use blueprint_runner::config::BlueprintEnvironment;
8use eigensdk::client_elcontracts::reader::ELChainReader;
9use eigensdk::utils::rewardsv2::core::rewards_coordinator::{
10    IRewardsCoordinator, RewardsCoordinator,
11};
12use std::str::FromStr;
13
14/// Manager for operator rewards claiming and tracking
15///
16/// Provides high-level abstractions for interacting with EigenLayer's RewardsCoordinator
17/// contract, handling rewards claiming, querying, and earnings calculation per strategy.
18#[derive(Clone)]
19pub struct RewardsManager {
20    env: BlueprintEnvironment,
21}
22
23impl RewardsManager {
24    /// Create a new RewardsManager
25    pub fn new(env: BlueprintEnvironment) -> Self {
26        Self { env }
27    }
28
29    /// Get the operator address from keystore
30    ///
31    /// # Errors
32    ///
33    /// * Keystore errors if ECDSA key not found or cannot be exposed
34    fn get_operator_address(&self) -> Result<Address> {
35        let ecdsa_public = self
36            .env
37            .keystore()
38            .first_local::<K256Ecdsa>()
39            .map_err(EigenlayerExtraError::Keystore)?;
40
41        let ecdsa_secret = self
42            .env
43            .keystore()
44            .expose_ecdsa_secret(&ecdsa_public)
45            .map_err(EigenlayerExtraError::Keystore)?
46            .ok_or_else(|| {
47                EigenlayerExtraError::InvalidConfiguration("No ECDSA secret found".into())
48            })?;
49
50        ecdsa_secret
51            .alloy_address()
52            .map_err(|e| EigenlayerExtraError::InvalidConfiguration(e.to_string()))
53    }
54
55    /// Get claimable rewards for the operator
56    ///
57    /// Returns the total amount of rewards that can be claimed by the operator
58    /// across all strategies.
59    ///
60    /// # Errors
61    ///
62    /// * Contract interaction errors
63    /// * Configuration errors if EigenLayer settings not found
64    /// * Keystore errors
65    pub async fn get_claimable_rewards(&self) -> Result<U256> {
66        let contract_addresses = self
67            .env
68            .protocol_settings
69            .eigenlayer()
70            .map_err(|e| EigenlayerExtraError::InvalidConfiguration(e.to_string()))?;
71
72        let el_chain_reader = ELChainReader::new(
73            Some(contract_addresses.allocation_manager_address),
74            contract_addresses.delegation_manager_address,
75            contract_addresses.rewards_coordinator_address,
76            contract_addresses.avs_directory_address,
77            Some(contract_addresses.permission_controller_address),
78            self.env.http_rpc_endpoint.to_string(),
79        );
80
81        // Get current claimable distribution root
82        let distribution_root = el_chain_reader
83            .get_current_claimable_distribution_root()
84            .await
85            .map_err(|e| EigenlayerExtraError::EigenSdk(e.to_string()))?;
86
87        info!(
88            "Current claimable distribution root: {} (activated at {})",
89            distribution_root.root, distribution_root.activatedAt
90        );
91
92        // Note: Actual rewards claiming requires:
93        // 1. Fetching rewards data from EigenLayer Sidecar (gRPC/HTTP indexer)
94        //    - List distribution roots: GET /rewards/v1/distribution-roots
95        //    - Get rewards for root: GET /rewards/v1/distribution-roots/{rootIndex}/rewards
96        // 2. Set claimer address if not already set (call set_claimer_for)
97        // 3. Submit claim with Merkle proof (call process_claim or process_claims)
98        //
99        // The S3 bucket approach is deprecated. Future implementation should integrate
100        // with the Sidecar gRPC/HTTP API to automate claiming.
101        //
102        // For now, we return the timestamp of the latest claimable root as a signal
103        // that rewards are available. Operators can manually claim via EigenLayer CLI
104        // or we can implement automated claiming in a future update.
105
106        Ok(U256::from(distribution_root.activatedAt))
107    }
108
109    /// Calculate earnings per strategy for the operator
110    ///
111    /// Returns a mapping of strategy addresses to share amounts.
112    /// These shares represent the operator's stake in each strategy.
113    ///
114    /// # Errors
115    ///
116    /// * Contract interaction errors
117    /// * Configuration errors
118    /// * Keystore errors
119    pub async fn calculate_earnings_per_strategy(
120        &self,
121    ) -> Result<alloc::vec::Vec<(Address, U256)>> {
122        let operator_address = self.get_operator_address()?;
123        let contract_addresses = self
124            .env
125            .protocol_settings
126            .eigenlayer()
127            .map_err(|e| EigenlayerExtraError::InvalidConfiguration(e.to_string()))?;
128
129        let el_chain_reader = ELChainReader::new(
130            Some(contract_addresses.allocation_manager_address),
131            contract_addresses.delegation_manager_address,
132            contract_addresses.rewards_coordinator_address,
133            contract_addresses.avs_directory_address,
134            Some(contract_addresses.permission_controller_address),
135            self.env.http_rpc_endpoint.to_string(),
136        );
137
138        // Get operator's deposited shares (strategies and amounts)
139        let (strategies, shares) = el_chain_reader
140            .get_staker_shares(operator_address)
141            .await
142            .map_err(|e| EigenlayerExtraError::EigenSdk(e.to_string()))?;
143
144        // Combine strategies with their corresponding shares
145        let mut earnings = alloc::vec::Vec::with_capacity(strategies.len());
146        for (strategy, share) in strategies.into_iter().zip(shares.into_iter()) {
147            if !share.is_zero() {
148                earnings.push((strategy, share));
149                info!(
150                    "Operator {} has {} shares in strategy {}",
151                    operator_address, share, strategy
152                );
153            }
154        }
155
156        Ok(earnings)
157    }
158
159    /// Claim a single reward for the operator
160    ///
161    /// Submits a transaction to claim a specific reward for the operator.
162    /// For claiming multiple rewards at once (gas optimization), use `claim_rewards_batch()`.
163    ///
164    /// # Arguments
165    ///
166    /// * `root` - Merkle root for the rewards distribution
167    /// * `reward_claim` - The reward claim data structure
168    ///
169    /// # Errors
170    ///
171    /// * Transaction errors
172    /// * Configuration errors
173    /// * No rewards available to claim
174    #[allow(dead_code)]
175    pub async fn claim_rewards(
176        &self,
177        _root: FixedBytes<32>,
178        reward_claim: IRewardsCoordinator::RewardsMerkleClaim,
179    ) -> Result<FixedBytes<32>> {
180        let contract_addresses = self
181            .env
182            .protocol_settings
183            .eigenlayer()
184            .map_err(|e| EigenlayerExtraError::InvalidConfiguration(e.to_string()))?;
185
186        let operator_address = self.get_operator_address()?;
187        let ecdsa_public = self
188            .env
189            .keystore()
190            .first_local::<K256Ecdsa>()
191            .map_err(EigenlayerExtraError::Keystore)?;
192        let ecdsa_secret = self
193            .env
194            .keystore()
195            .expose_ecdsa_secret(&ecdsa_public)
196            .map_err(EigenlayerExtraError::Keystore)?
197            .ok_or_else(|| {
198                EigenlayerExtraError::InvalidConfiguration("No ECDSA secret found".into())
199            })?;
200
201        let private_key = alloy_primitives::hex::encode(ecdsa_secret.0.to_bytes());
202        let wallet = alloy_signer_local::PrivateKeySigner::from_str(&private_key)
203            .map_err(|e| EigenlayerExtraError::InvalidConfiguration(e.to_string()))?;
204
205        let provider = blueprint_evm_extra::util::get_wallet_provider_http(
206            self.env.http_rpc_endpoint.clone(),
207            alloy_network::EthereumWallet::from(wallet),
208        );
209
210        let rewards_coordinator =
211            RewardsCoordinator::new(contract_addresses.rewards_coordinator_address, provider);
212
213        // Process the claim
214        let receipt = rewards_coordinator
215            .processClaim(reward_claim, operator_address)
216            .send()
217            .await
218            .map_err(|e| EigenlayerExtraError::Transaction(e.to_string()))?
219            .get_receipt()
220            .await
221            .map_err(|e| EigenlayerExtraError::Transaction(e.to_string()))?;
222
223        info!(
224            "Rewards claimed successfully: {:?}",
225            receipt.transaction_hash
226        );
227
228        Ok(receipt.transaction_hash)
229    }
230
231    /// Batch claim multiple rewards for the operator (gas optimized)
232    ///
233    /// Submits a single transaction to claim multiple rewards at once.
234    /// This is more gas efficient than calling `claim_rewards()` multiple times.
235    ///
236    /// This matches the gas optimization used by the EigenLayer CLI's batch claiming feature.
237    ///
238    /// # Arguments
239    ///
240    /// * `reward_claims` - Vector of reward claim data structures
241    ///
242    /// # Errors
243    ///
244    /// * Transaction errors
245    /// * Configuration errors
246    /// * No rewards available to claim
247    #[allow(dead_code)]
248    pub async fn claim_rewards_batch(
249        &self,
250        reward_claims: Vec<IRewardsCoordinator::RewardsMerkleClaim>,
251    ) -> Result<FixedBytes<32>> {
252        if reward_claims.is_empty() {
253            return Err(EigenlayerExtraError::InvalidConfiguration(
254                "No reward claims provided".into(),
255            ));
256        }
257
258        let contract_addresses = self
259            .env
260            .protocol_settings
261            .eigenlayer()
262            .map_err(|e| EigenlayerExtraError::InvalidConfiguration(e.to_string()))?;
263
264        let operator_address = self.get_operator_address()?;
265        let ecdsa_public = self
266            .env
267            .keystore()
268            .first_local::<K256Ecdsa>()
269            .map_err(EigenlayerExtraError::Keystore)?;
270        let ecdsa_secret = self
271            .env
272            .keystore()
273            .expose_ecdsa_secret(&ecdsa_public)
274            .map_err(EigenlayerExtraError::Keystore)?
275            .ok_or_else(|| {
276                EigenlayerExtraError::InvalidConfiguration("No ECDSA secret found".into())
277            })?;
278
279        let private_key = alloy_primitives::hex::encode(ecdsa_secret.0.to_bytes());
280        let wallet = alloy_signer_local::PrivateKeySigner::from_str(&private_key)
281            .map_err(|e| EigenlayerExtraError::InvalidConfiguration(e.to_string()))?;
282
283        let provider = blueprint_evm_extra::util::get_wallet_provider_http(
284            self.env.http_rpc_endpoint.clone(),
285            alloy_network::EthereumWallet::from(wallet),
286        );
287
288        let rewards_coordinator =
289            RewardsCoordinator::new(contract_addresses.rewards_coordinator_address, provider);
290
291        // Process all claims in a single transaction (gas optimization)
292        let receipt = rewards_coordinator
293            .processClaims(reward_claims.clone(), operator_address)
294            .send()
295            .await
296            .map_err(|e| EigenlayerExtraError::Transaction(e.to_string()))?
297            .get_receipt()
298            .await
299            .map_err(|e| EigenlayerExtraError::Transaction(e.to_string()))?;
300
301        info!(
302            "Batch claimed {} rewards successfully: {:?}",
303            reward_claims.len(),
304            receipt.transaction_hash
305        );
306
307        Ok(receipt.transaction_hash)
308    }
309
310    /// Check if operator is registered
311    ///
312    /// # Errors
313    ///
314    /// * Configuration errors
315    /// * Contract interaction errors
316    pub async fn is_operator_registered(&self) -> Result<bool> {
317        let operator_address = self.get_operator_address()?;
318        let contract_addresses = self
319            .env
320            .protocol_settings
321            .eigenlayer()
322            .map_err(|e| EigenlayerExtraError::InvalidConfiguration(e.to_string()))?;
323
324        let el_chain_reader = ELChainReader::new(
325            Some(contract_addresses.allocation_manager_address),
326            contract_addresses.delegation_manager_address,
327            contract_addresses.rewards_coordinator_address,
328            contract_addresses.avs_directory_address,
329            Some(contract_addresses.permission_controller_address),
330            self.env.http_rpc_endpoint.to_string(),
331        );
332
333        el_chain_reader
334            .is_operator_registered(operator_address)
335            .await
336            .map_err(|e| EigenlayerExtraError::EigenSdk(e.to_string()))
337    }
338}
339
340#[cfg(test)]
341mod tests {
342
343    #[tokio::test]
344    #[ignore] // Requires EigenLayer deployment
345    async fn test_rewards_manager_creation() {
346        // This test would require a full BlueprintEnvironment setup
347        // with EigenLayer contract addresses
348    }
349}