openzeppelin_relayer/services/gas/
evm_gas_price.rs

1//! This module provides services for estimating gas prices on the Ethereum Virtual Machine (EVM).
2//! It includes traits and implementations for calculating gas price multipliers based on
3//! transaction speed and fetching gas prices using JSON-RPC.
4use crate::{
5    models::{evm::Speed, EvmNetwork, EvmTransactionData, TransactionError},
6    services::EvmProviderTrait,
7};
8use alloy::rpc::types::BlockNumberOrTag;
9use eyre::Result;
10use futures::try_join;
11use log::info;
12
13use async_trait::async_trait;
14use itertools::Itertools;
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17
18#[cfg(test)]
19use mockall::automock;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct SpeedPrices {
23    pub safe_low: u128,
24    pub average: u128,
25    pub fast: u128,
26    pub fastest: u128,
27}
28
29#[cfg(test)]
30impl Default for SpeedPrices {
31    fn default() -> Self {
32        Self {
33            safe_low: 20_000_000_000, // 20 Gwei
34            average: 30_000_000_000,  // 30 Gwei
35            fast: 40_000_000_000,     // 40 Gwei
36            fastest: 50_000_000_000,  // 50 Gwei
37        }
38    }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct GasPrices {
43    pub legacy_prices: SpeedPrices,
44    pub max_priority_fee_per_gas: SpeedPrices,
45    pub base_fee_per_gas: u128,
46}
47
48#[cfg(test)]
49impl Default for GasPrices {
50    fn default() -> Self {
51        Self {
52            legacy_prices: SpeedPrices::default(),
53            max_priority_fee_per_gas: SpeedPrices::default(),
54            base_fee_per_gas: 10_000_000_000, // 10 Gwei base fee
55        }
56    }
57}
58
59impl std::cmp::Eq for Speed {}
60
61impl std::hash::Hash for Speed {
62    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
63        core::mem::discriminant(self).hash(state);
64    }
65}
66
67const GWEI: f64 = 1e9;
68
69// calculate the multiplier for the gas estimation
70impl Speed {
71    pub fn multiplier() -> [(Speed, u128); 4] {
72        [
73            (Speed::SafeLow, 100),
74            (Speed::Average, 125),
75            (Speed::Fast, 150),
76            (Speed::Fastest, 200),
77        ]
78    }
79}
80
81impl IntoIterator for GasPrices {
82    type Item = (Speed, u128, u128);
83    type IntoIter = std::vec::IntoIter<Self::Item>;
84
85    fn into_iter(self) -> Self::IntoIter {
86        let speeds = [Speed::SafeLow, Speed::Average, Speed::Fast, Speed::Fastest];
87
88        speeds
89            .into_iter()
90            .map(|speed| {
91                let max_fee = match speed {
92                    Speed::SafeLow => self.legacy_prices.safe_low,
93                    Speed::Average => self.legacy_prices.average,
94                    Speed::Fast => self.legacy_prices.fast,
95                    Speed::Fastest => self.legacy_prices.fastest,
96                };
97
98                let max_priority_fee = match speed {
99                    Speed::SafeLow => self.max_priority_fee_per_gas.safe_low,
100                    Speed::Average => self.max_priority_fee_per_gas.average,
101                    Speed::Fast => self.max_priority_fee_per_gas.fast,
102                    Speed::Fastest => self.max_priority_fee_per_gas.fastest,
103                };
104
105                (speed, max_fee, max_priority_fee)
106            })
107            .collect::<Vec<_>>()
108            .into_iter()
109    }
110}
111
112impl IntoIterator for SpeedPrices {
113    type Item = (Speed, u128);
114    type IntoIter = std::vec::IntoIter<Self::Item>;
115
116    fn into_iter(self) -> Self::IntoIter {
117        vec![
118            (Speed::SafeLow, self.safe_low),
119            (Speed::Average, self.average),
120            (Speed::Fast, self.fast),
121            (Speed::Fastest, self.fastest),
122        ]
123        .into_iter()
124    }
125}
126
127#[async_trait]
128#[cfg_attr(test, automock(
129    type Provider = crate::services::MockEvmProviderTrait;
130))]
131#[allow(dead_code)]
132pub trait EvmGasPriceServiceTrait {
133    type Provider: EvmProviderTrait;
134
135    async fn estimate_gas(&self, tx_data: &EvmTransactionData) -> Result<u64, TransactionError>;
136
137    async fn get_legacy_prices_from_json_rpc(&self) -> Result<SpeedPrices, TransactionError>;
138
139    async fn get_prices_from_json_rpc(&self) -> Result<GasPrices, TransactionError>;
140
141    async fn get_current_base_fee(&self) -> Result<u128, TransactionError>;
142
143    fn network(&self) -> &EvmNetwork;
144}
145
146pub struct EvmGasPriceService<P: EvmProviderTrait> {
147    provider: P,
148    network: EvmNetwork,
149}
150
151impl<P: EvmProviderTrait> EvmGasPriceService<P> {
152    pub fn new(provider: P, network: EvmNetwork) -> Self {
153        Self { provider, network }
154    }
155
156    pub fn network(&self) -> &EvmNetwork {
157        &self.network
158    }
159}
160
161#[async_trait]
162impl<P: EvmProviderTrait> EvmGasPriceServiceTrait for EvmGasPriceService<P> {
163    type Provider = P;
164
165    async fn estimate_gas(&self, tx_data: &EvmTransactionData) -> Result<u64, TransactionError> {
166        info!("Estimating gas for tx_data: {:?}", tx_data);
167        let gas_estimation = self.provider.estimate_gas(tx_data).await.map_err(|err| {
168            let msg = format!("Failed to estimate gas: {err}");
169            TransactionError::NetworkConfiguration(msg)
170        })?;
171        Ok(gas_estimation)
172    }
173
174    async fn get_legacy_prices_from_json_rpc(&self) -> Result<SpeedPrices, TransactionError> {
175        let base = self.provider.get_gas_price().await?;
176        let prices: Vec<(Speed, u128)> = Speed::multiplier()
177            .into_iter()
178            .map(|(speed, multiplier)| {
179                let final_gas = (base * multiplier) / 100;
180                (speed, final_gas)
181            })
182            .collect();
183
184        Ok(SpeedPrices {
185            safe_low: prices
186                .iter()
187                .find(|(s, _)| *s == Speed::SafeLow)
188                .map(|(_, p)| *p)
189                .unwrap_or(0),
190            average: prices
191                .iter()
192                .find(|(s, _)| *s == Speed::Average)
193                .map(|(_, p)| *p)
194                .unwrap_or(0),
195            fast: prices
196                .iter()
197                .find(|(s, _)| *s == Speed::Fast)
198                .map(|(_, p)| *p)
199                .unwrap_or(0),
200            fastest: prices
201                .iter()
202                .find(|(s, _)| *s == Speed::Fastest)
203                .map(|(_, p)| *p)
204                .unwrap_or(0),
205        })
206    }
207
208    async fn get_current_base_fee(&self) -> Result<u128, TransactionError> {
209        let block = self.provider.get_block_by_number().await?;
210        let base_fee = block.header.base_fee_per_gas.unwrap_or(0);
211        Ok(base_fee.into())
212    }
213
214    async fn get_prices_from_json_rpc(&self) -> Result<GasPrices, TransactionError> {
215        const HISTORICAL_BLOCKS: u64 = 4;
216
217        // Define speed percentiles
218        let speed_percentiles: HashMap<Speed, (usize, f64)> = [
219            (Speed::SafeLow, (0, 30.0)),
220            (Speed::Average, (1, 50.0)),
221            (Speed::Fast, (2, 85.0)),
222            (Speed::Fastest, (3, 99.0)),
223        ]
224        .into();
225
226        // Create array of reward percentiles
227        let reward_percentiles: Vec<f64> = speed_percentiles
228            .values()
229            .sorted_by_key(|&(idx, _)| idx)
230            .map(|(_, percentile)| *percentile)
231            .collect();
232
233        // Get prices in parallel
234        let (legacy_prices, base_fee, fee_history) = try_join!(
235            self.get_legacy_prices_from_json_rpc(),
236            self.get_current_base_fee(),
237            async {
238                self.provider
239                    .get_fee_history(
240                        HISTORICAL_BLOCKS,
241                        BlockNumberOrTag::Latest,
242                        reward_percentiles,
243                    )
244                    .await
245                    .map_err(|e| {
246                        TransactionError::NetworkConfiguration(format!(
247                            "Failed to fetch fee history data: {}",
248                            e
249                        ))
250                    })
251            }
252        )?;
253
254        // Calculate maxPriorityFeePerGas for each speed
255        let max_priority_fees: HashMap<Speed, f64> = Speed::multiplier()
256            .into_iter()
257            .filter_map(|(speed, _)| {
258                let (idx, percentile) = speed_percentiles.get(&speed)?;
259
260                // Get rewards for this speed's percentile
261                let rewards: Vec<f64> = fee_history
262                    .reward
263                    .as_ref()
264                    .map(|rewards| {
265                        rewards
266                            .iter()
267                            .filter_map(|block_rewards| {
268                                let reward = block_rewards[*idx];
269                                if reward > 0 {
270                                    Some(reward as f64 / GWEI)
271                                } else {
272                                    None
273                                }
274                            })
275                            .collect()
276                    })
277                    .unwrap_or_default();
278
279                // Calculate mean of non-zero rewards, or use fallback
280                let priority_fee = if rewards.is_empty() {
281                    // Fallback: 1 gwei * percentile / 100
282                    (1.0 * percentile) / 100.0
283                } else {
284                    rewards.iter().sum::<f64>() / rewards.len() as f64
285                };
286
287                Some((speed, priority_fee))
288            })
289            .collect();
290
291        // Convert max_priority_fees to SpeedPrices
292        let max_priority_fees = SpeedPrices {
293            safe_low: (max_priority_fees.get(&Speed::SafeLow).unwrap_or(&0.0) * GWEI) as u128,
294            average: (max_priority_fees.get(&Speed::Average).unwrap_or(&0.0) * GWEI) as u128,
295            fast: (max_priority_fees.get(&Speed::Fast).unwrap_or(&0.0) * GWEI) as u128,
296            fastest: (max_priority_fees.get(&Speed::Fastest).unwrap_or(&0.0) * GWEI) as u128,
297        };
298
299        Ok(GasPrices {
300            legacy_prices,
301            max_priority_fee_per_gas: max_priority_fees,
302            base_fee_per_gas: base_fee,
303        })
304    }
305
306    fn network(&self) -> &EvmNetwork {
307        &self.network
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use alloy::rpc::types::FeeHistory;
314
315    use crate::services::provider::evm::MockEvmProviderTrait;
316    use alloy::rpc::types::{Block as BlockResponse, Header};
317
318    use super::*;
319
320    fn create_test_evm_network() -> EvmNetwork {
321        EvmNetwork {
322            network: "mainnet".to_string(),
323            rpc_urls: vec!["https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY".to_string()],
324            explorer_urls: None,
325            average_blocktime_ms: 12000,
326            is_testnet: false,
327            tags: vec!["mainnet".to_string()],
328            chain_id: 1,
329            required_confirmations: 1,
330            features: vec!["eip1559".to_string()],
331            symbol: "ETH".to_string(),
332        }
333    }
334
335    #[test]
336    fn test_speed_multiplier() {
337        let multipliers = Speed::multiplier();
338        assert_eq!(multipliers.len(), 4);
339        assert_eq!(multipliers[0], (Speed::SafeLow, 100));
340        assert_eq!(multipliers[1], (Speed::Average, 125));
341        assert_eq!(multipliers[2], (Speed::Fast, 150));
342        assert_eq!(multipliers[3], (Speed::Fastest, 200));
343    }
344
345    #[test]
346    fn test_gas_prices_into_iterator() {
347        let gas_prices = GasPrices {
348            legacy_prices: SpeedPrices {
349                safe_low: 10,
350                average: 20,
351                fast: 30,
352                fastest: 40,
353            },
354            max_priority_fee_per_gas: SpeedPrices {
355                safe_low: 1,
356                average: 2,
357                fast: 3,
358                fastest: 4,
359            },
360            base_fee_per_gas: 100,
361        };
362
363        let prices: Vec<(Speed, u128, u128)> = gas_prices.into_iter().collect();
364        assert_eq!(prices.len(), 4);
365        assert_eq!(prices[0], (Speed::SafeLow, 10, 1));
366        assert_eq!(prices[1], (Speed::Average, 20, 2));
367        assert_eq!(prices[2], (Speed::Fast, 30, 3));
368        assert_eq!(prices[3], (Speed::Fastest, 40, 4));
369    }
370
371    #[test]
372    fn test_speed_prices_into_iterator() {
373        let speed_prices = SpeedPrices {
374            safe_low: 10,
375            average: 20,
376            fast: 30,
377            fastest: 40,
378        };
379
380        let prices: Vec<(Speed, u128)> = speed_prices.into_iter().collect();
381        assert_eq!(prices.len(), 4);
382        assert_eq!(prices[0], (Speed::SafeLow, 10));
383        assert_eq!(prices[1], (Speed::Average, 20));
384        assert_eq!(prices[2], (Speed::Fast, 30));
385        assert_eq!(prices[3], (Speed::Fastest, 40));
386    }
387
388    #[tokio::test]
389    async fn test_get_legacy_prices_from_json_rpc() {
390        let mut mock_provider = MockEvmProviderTrait::new();
391        let base_gas_price = 10_000_000_000u128; // 10 gwei base price
392
393        // Mock the provider's get_gas_price method
394        mock_provider
395            .expect_get_gas_price()
396            .times(1)
397            .returning(move || Box::pin(async move { Ok(base_gas_price) }));
398
399        // Create the actual service with mocked provider
400        let service = EvmGasPriceService::new(mock_provider, create_test_evm_network());
401
402        // Test the actual implementation
403        let prices = service.get_legacy_prices_from_json_rpc().await.unwrap();
404
405        // Verify each speed level has correct multiplier applied
406        assert_eq!(prices.safe_low, 10_000_000_000); // 10 gwei * 100%
407        assert_eq!(prices.average, 12_500_000_000); // 10 gwei * 125%
408        assert_eq!(prices.fast, 15_000_000_000); // 10 gwei * 150%
409        assert_eq!(prices.fastest, 20_000_000_000); // 10 gwei * 200%
410
411        // Verify against Speed::multiplier()
412        let multipliers = Speed::multiplier();
413        for (speed, multiplier) in multipliers.iter() {
414            let price = match speed {
415                Speed::SafeLow => prices.safe_low,
416                Speed::Average => prices.average,
417                Speed::Fast => prices.fast,
418                Speed::Fastest => prices.fastest,
419            };
420            assert_eq!(
421                price,
422                base_gas_price * multiplier / 100,
423                "Price for {:?} should be {}% of base price",
424                speed,
425                multiplier
426            );
427        }
428    }
429
430    #[tokio::test]
431    async fn test_get_current_base_fee() {
432        let mut mock_provider = MockEvmProviderTrait::new();
433        let expected_base_fee = 10_000_000_000u128;
434
435        // Mock the provider's get_block_by_number method
436        mock_provider
437            .expect_get_block_by_number()
438            .times(1)
439            .returning(move || {
440                Box::pin(async move {
441                    Ok(BlockResponse {
442                        header: Header {
443                            inner: alloy::consensus::Header {
444                                base_fee_per_gas: Some(expected_base_fee as u64),
445                                ..Default::default()
446                            },
447                            ..Default::default()
448                        },
449                        ..Default::default()
450                    })
451                })
452            });
453
454        let service = EvmGasPriceService::new(mock_provider, create_test_evm_network());
455        let result = service.get_current_base_fee().await.unwrap();
456        assert_eq!(result, expected_base_fee);
457    }
458
459    #[tokio::test]
460    async fn test_get_prices_from_json_rpc() {
461        let mut mock_provider = MockEvmProviderTrait::new();
462        let base_gas_price = 10_000_000_000u128;
463        let base_fee = 5_000_000_000u128;
464
465        // Mock get_gas_price for legacy prices
466        mock_provider
467            .expect_get_gas_price()
468            .times(1)
469            .returning(move || Box::pin(async move { Ok(base_gas_price) }));
470
471        // Mock get_block_by_number for base fee
472        mock_provider
473            .expect_get_block_by_number()
474            .times(1)
475            .returning(move || {
476                Box::pin(async move {
477                    Ok(BlockResponse {
478                        header: Header {
479                            inner: alloy::consensus::Header {
480                                base_fee_per_gas: Some(base_fee as u64),
481                                ..Default::default()
482                            },
483                            ..Default::default()
484                        },
485                        ..Default::default()
486                    })
487                })
488            });
489
490        // Mock get_fee_history
491        mock_provider
492            .expect_get_fee_history()
493            .times(1)
494            .returning(|_, _, _| {
495                Box::pin(async {
496                    Ok(FeeHistory {
497                        oldest_block: 100,
498                        base_fee_per_gas: vec![5_000_000_000],
499                        gas_used_ratio: vec![0.5],
500                        reward: Some(vec![vec![
501                            1_000_000_000,
502                            2_000_000_000,
503                            3_000_000_000,
504                            4_000_000_000,
505                        ]]),
506                        base_fee_per_blob_gas: vec![],
507                        blob_gas_used_ratio: vec![],
508                    })
509                })
510            });
511
512        let service = EvmGasPriceService::new(mock_provider, create_test_evm_network());
513        let prices = service.get_prices_from_json_rpc().await.unwrap();
514
515        // Test legacy prices
516        assert_eq!(prices.legacy_prices.safe_low, 10_000_000_000);
517        assert_eq!(prices.legacy_prices.average, 12_500_000_000);
518        assert_eq!(prices.legacy_prices.fast, 15_000_000_000);
519        assert_eq!(prices.legacy_prices.fastest, 20_000_000_000);
520
521        // Test base fee
522        assert_eq!(prices.base_fee_per_gas, 5_000_000_000);
523
524        // Test priority fees
525        assert_eq!(prices.max_priority_fee_per_gas.safe_low, 1_000_000_000);
526        assert_eq!(prices.max_priority_fee_per_gas.average, 2_000_000_000);
527        assert_eq!(prices.max_priority_fee_per_gas.fast, 3_000_000_000);
528        assert_eq!(prices.max_priority_fee_per_gas.fastest, 4_000_000_000);
529    }
530}