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