1use 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, 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::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 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: {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; mock_provider
489 .expect_get_gas_price()
490 .times(1)
491 .returning(move || Box::pin(async move { Ok(base_gas_price) }));
492
493 let service = EvmGasPriceService::new(mock_provider, create_test_evm_network(), None);
495
496 let prices = service.get_legacy_prices_from_json_rpc().await.unwrap();
498
499 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();
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_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_provider
554 .expect_get_gas_price()
555 .times(1)
556 .returning(move || Box::pin(async move { Ok(base_gas_price) }));
557
558 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_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 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 assert_eq!(prices.base_fee_per_gas, 5_000_000_000);
603
604 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}