1use 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, average: 30_000_000_000, fast: 40_000_000_000, fastest: 50_000_000_000, }
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, }
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
69impl 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 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 let reward_percentiles: Vec<f64> = speed_percentiles
228 .values()
229 .sorted_by_key(|&(idx, _)| idx)
230 .map(|(_, percentile)| *percentile)
231 .collect();
232
233 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 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 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 let priority_fee = if rewards.is_empty() {
281 (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 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; mock_provider
395 .expect_get_gas_price()
396 .times(1)
397 .returning(move || Box::pin(async move { Ok(base_gas_price) }));
398
399 let service = EvmGasPriceService::new(mock_provider, create_test_evm_network());
401
402 let prices = service.get_legacy_prices_from_json_rpc().await.unwrap();
404
405 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();
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_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_provider
467 .expect_get_gas_price()
468 .times(1)
469 .returning(move || Box::pin(async move { Ok(base_gas_price) }));
470
471 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_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 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 assert_eq!(prices.base_fee_per_gas, 5_000_000_000);
523
524 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}