1use crate::{
5 constants::HISTORICAL_BLOCKS,
6 models::{evm::Speed, EvmNetwork, EvmTransactionData, TransactionError},
7 services::{
8 gas::{cache::GasPriceCache, fetchers::GasPriceFetcherFactory},
9 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, average: 30_000_000_000, fast: 40_000_000_000, fastest: 50_000_000_000, }
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, }
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
76const GWEI: f64 = 1e9;
78
79impl 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::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 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 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: {}",
383 e
384 ))
385 })
386 }
387 )?;
388
389 let max_priority_fees = Self::compute_max_priority_fees_from_history(&fee_history);
390
391 Ok(GasPrices {
392 legacy_prices,
393 max_priority_fee_per_gas: max_priority_fees,
394 base_fee_per_gas: base_fee,
395 })
396 }
397
398 fn network(&self) -> &EvmNetwork {
399 &self.network
400 }
401}
402
403#[cfg(test)]
404mod tests {
405 use alloy::{
406 network::AnyRpcBlock,
407 rpc::types::{Block, FeeHistory},
408 };
409
410 use crate::services::provider::evm::MockEvmProviderTrait;
411
412 use super::*;
413
414 fn create_test_evm_network() -> EvmNetwork {
415 EvmNetwork {
416 network: "mainnet".to_string(),
417 rpc_urls: vec!["https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY".to_string()],
418 explorer_urls: None,
419 average_blocktime_ms: 12000,
420 is_testnet: false,
421 tags: vec!["mainnet".to_string()],
422 chain_id: 1,
423 required_confirmations: 1,
424 features: vec!["eip1559".to_string()],
425 symbol: "ETH".to_string(),
426 gas_price_cache: None,
427 }
428 }
429
430 #[test]
431 fn test_speed_multiplier() {
432 let multipliers = Speed::multiplier();
433 assert_eq!(multipliers.len(), 4);
434 assert_eq!(multipliers[0], (Speed::SafeLow, 100));
435 assert_eq!(multipliers[1], (Speed::Average, 125));
436 assert_eq!(multipliers[2], (Speed::Fast, 150));
437 assert_eq!(multipliers[3], (Speed::Fastest, 200));
438 }
439
440 #[test]
441 fn test_gas_prices_into_iterator() {
442 let gas_prices = GasPrices {
443 legacy_prices: SpeedPrices {
444 safe_low: 10,
445 average: 20,
446 fast: 30,
447 fastest: 40,
448 },
449 max_priority_fee_per_gas: SpeedPrices {
450 safe_low: 1,
451 average: 2,
452 fast: 3,
453 fastest: 4,
454 },
455 base_fee_per_gas: 100,
456 };
457
458 let prices: Vec<(Speed, u128, u128)> = gas_prices.into_iter().collect();
459 assert_eq!(prices.len(), 4);
460 assert_eq!(prices[0], (Speed::SafeLow, 10, 1));
461 assert_eq!(prices[1], (Speed::Average, 20, 2));
462 assert_eq!(prices[2], (Speed::Fast, 30, 3));
463 assert_eq!(prices[3], (Speed::Fastest, 40, 4));
464 }
465
466 #[test]
467 fn test_speed_prices_into_iterator() {
468 let speed_prices = SpeedPrices {
469 safe_low: 10,
470 average: 20,
471 fast: 30,
472 fastest: 40,
473 };
474
475 let prices: Vec<(Speed, u128)> = speed_prices.into_iter().collect();
476 assert_eq!(prices.len(), 4);
477 assert_eq!(prices[0], (Speed::SafeLow, 10));
478 assert_eq!(prices[1], (Speed::Average, 20));
479 assert_eq!(prices[2], (Speed::Fast, 30));
480 assert_eq!(prices[3], (Speed::Fastest, 40));
481 }
482
483 #[tokio::test]
484 async fn test_get_legacy_prices_from_json_rpc() {
485 let mut mock_provider = MockEvmProviderTrait::new();
486 let base_gas_price = 10_000_000_000u128; mock_provider
490 .expect_get_gas_price()
491 .times(1)
492 .returning(move || Box::pin(async move { Ok(base_gas_price) }));
493
494 let service = EvmGasPriceService::new(mock_provider, create_test_evm_network(), None);
496
497 let prices = service.get_legacy_prices_from_json_rpc().await.unwrap();
499
500 assert_eq!(prices.safe_low, 10_000_000_000); assert_eq!(prices.average, 12_500_000_000); assert_eq!(prices.fast, 15_000_000_000); assert_eq!(prices.fastest, 20_000_000_000); let multipliers = Speed::multiplier();
508 for (speed, multiplier) in multipliers.iter() {
509 let price = match speed {
510 Speed::SafeLow => prices.safe_low,
511 Speed::Average => prices.average,
512 Speed::Fast => prices.fast,
513 Speed::Fastest => prices.fastest,
514 };
515 assert_eq!(
516 price,
517 base_gas_price * multiplier / 100,
518 "Price for {:?} should be {}% of base price",
519 speed,
520 multiplier
521 );
522 }
523 }
524
525 #[tokio::test]
526 async fn test_get_current_base_fee() {
527 let mut mock_provider = MockEvmProviderTrait::new();
528 let expected_base_fee = 10_000_000_000u128;
529
530 mock_provider
532 .expect_get_block_by_number()
533 .times(1)
534 .returning(move || {
535 Box::pin(async move {
536 let mut block: Block = Block::default();
537 block.header.base_fee_per_gas = Some(expected_base_fee as u64);
538 Ok(AnyRpcBlock::from(block))
539 })
540 });
541
542 let service = EvmGasPriceService::new(mock_provider, create_test_evm_network(), None);
543 let result = service.get_current_base_fee().await.unwrap();
544 assert_eq!(result, expected_base_fee);
545 }
546
547 #[tokio::test]
548 async fn test_get_prices_from_json_rpc() {
549 let mut mock_provider = MockEvmProviderTrait::new();
550 let base_gas_price = 10_000_000_000u128;
551 let base_fee = 5_000_000_000u128;
552
553 mock_provider
555 .expect_get_gas_price()
556 .times(1)
557 .returning(move || Box::pin(async move { Ok(base_gas_price) }));
558
559 mock_provider
561 .expect_get_block_by_number()
562 .times(1)
563 .returning(move || {
564 Box::pin(async move {
565 let mut block: Block = Block::default();
566 block.header.base_fee_per_gas = Some(base_fee as u64);
567 Ok(AnyRpcBlock::from(block))
568 })
569 });
570
571 mock_provider
573 .expect_get_fee_history()
574 .times(1)
575 .returning(|_, _, _| {
576 Box::pin(async {
577 Ok(FeeHistory {
578 oldest_block: 100,
579 base_fee_per_gas: vec![5_000_000_000],
580 gas_used_ratio: vec![0.5],
581 reward: Some(vec![vec![
582 1_000_000_000,
583 2_000_000_000,
584 3_000_000_000,
585 4_000_000_000,
586 ]]),
587 base_fee_per_blob_gas: vec![],
588 blob_gas_used_ratio: vec![],
589 })
590 })
591 });
592
593 let service = EvmGasPriceService::new(mock_provider, create_test_evm_network(), None);
594 let prices = service.get_prices_from_json_rpc().await.unwrap();
595
596 assert_eq!(prices.legacy_prices.safe_low, 10_000_000_000);
598 assert_eq!(prices.legacy_prices.average, 12_500_000_000);
599 assert_eq!(prices.legacy_prices.fast, 15_000_000_000);
600 assert_eq!(prices.legacy_prices.fastest, 20_000_000_000);
601
602 assert_eq!(prices.base_fee_per_gas, 5_000_000_000);
604
605 assert_eq!(prices.max_priority_fee_per_gas.safe_low, 1_000_000_000);
607 assert_eq!(prices.max_priority_fee_per_gas.average, 2_000_000_000);
608 assert_eq!(prices.max_priority_fee_per_gas.fast, 3_000_000_000);
609 assert_eq!(prices.max_priority_fee_per_gas.fastest, 4_000_000_000);
610 }
611}