ic_evm_utils/
fees.rs

1//! This module provides functions for estimating transaction fees and getting the fee history.
2use candid::Nat;
3use ethers_core::types::U256;
4use evm_rpc_canister_types::{
5    BlockTag, EvmRpcCanister, FeeHistory, FeeHistoryArgs, FeeHistoryResult, MultiFeeHistoryResult,
6    RpcServices,
7};
8use serde_bytes::ByteBuf;
9use std::ops::Add;
10
11use crate::conversions::nat_to_u256;
12
13/// The minimum suggested maximum priority fee per gas.
14const MIN_SUGGEST_MAX_PRIORITY_FEE_PER_GAS: u32 = 1_500_000_000;
15
16/// Gets the fee history.
17///
18/// # Arguments
19///
20/// * `block_count` - The number of blocks to get the fee history for.
21/// * `newest_block` - The newest block to get the fee history for.
22/// * `reward_percentiles` - The reward percentiles to get the fee history for.
23/// * `rpc_services` - The RPC services used to interact with the EVM.
24/// * `evm_rpc` - The EVM RPC canister.
25///
26/// # Returns
27///
28/// The fee history.
29pub async fn fee_history(
30    block_count: Nat,
31    newest_block: BlockTag,
32    reward_percentiles: Option<Vec<u8>>,
33    rpc_services: RpcServices,
34    evm_rpc: EvmRpcCanister,
35) -> FeeHistory {
36    let fee_history_args: FeeHistoryArgs = FeeHistoryArgs {
37        blockCount: block_count,
38        newestBlock: newest_block,
39        rewardPercentiles: reward_percentiles.map(ByteBuf::from),
40    };
41
42    let cycles = 10_000_000_000;
43
44    match evm_rpc
45        .eth_fee_history(rpc_services, None, fee_history_args, cycles)
46        .await
47    {
48        Ok((res,)) => match res {
49            MultiFeeHistoryResult::Consistent(fee_history) => match fee_history {
50                FeeHistoryResult::Ok(fee_history) => fee_history,
51                FeeHistoryResult::Err(e) => {
52                    ic_cdk::trap(format!("Error: {:?}", e).as_str());
53                }
54            },
55            MultiFeeHistoryResult::Inconsistent(_) => {
56                ic_cdk::trap("Fee history is inconsistent");
57            }
58        },
59        Err(e) => ic_cdk::trap(format!("Error: {:?}", e).as_str()),
60    }
61}
62
63/// Represents the fee estimates.
64pub struct FeeEstimates {
65    pub max_fee_per_gas: U256,
66    pub max_priority_fee_per_gas: U256,
67}
68
69/// Gets the median index.
70///
71/// # Arguments
72///
73/// * `length` - The length of the array.
74///
75/// # Returns
76///
77/// The median index.
78fn median_index(length: usize) -> usize {
79    if length == 0 {
80        panic!("Cannot find a median index for an array of length zero.");
81    }
82    (length - 1) / 2
83}
84
85/// Estimates the transaction fees.
86///
87/// # Arguments
88///
89/// * `block_count` - The number of historical blocks to base the fee estimates on.
90/// * `rpc_services` - The RPC services used to interact with the EVM.
91/// * `evm_rpc` - The EVM RPC canister.
92pub async fn estimate_transaction_fees(
93    block_count: u8,
94    rpc_services: RpcServices,
95    evm_rpc: EvmRpcCanister,
96) -> FeeEstimates {
97    // we are setting the `max_priority_fee_per_gas` based on this article:
98    // https://docs.alchemy.com/docs/maxpriorityfeepergas-vs-maxfeepergas
99    // following this logic, the base fee will be derived from the block history automatically
100    // and we only specify the maximum priority fee per gas (tip).
101    // the tip is derived from the fee history of the last 9 blocks, more specifically
102    // from the 95th percentile of the tip.
103    let fee_history = fee_history(
104        Nat::from(block_count),
105        BlockTag::Latest,
106        Some(vec![95]),
107        rpc_services,
108        evm_rpc,
109    )
110    .await;
111
112    let median_index = median_index(block_count.into());
113
114    // baseFeePerGas
115    let base_fee_per_gas = fee_history.baseFeePerGas.last().unwrap().clone();
116
117    // obtain the 95th percentile of the tips for the past 9 blocks
118    let mut percentile_95: Vec<Nat> = fee_history
119        .reward
120        .into_iter()
121        .flat_map(|x| x.into_iter())
122        .collect();
123    // sort the tips in ascending order
124    percentile_95.sort_unstable();
125    // get the median by accessing the element in the middle
126    // set tip to 0 if there are not enough blocks in case of a local testnet
127    let median_reward = percentile_95
128        .get(median_index)
129        .unwrap_or(&Nat::from(0_u8))
130        .clone();
131
132    let max_priority_fee_per_gas = median_reward
133        .clone()
134        .add(base_fee_per_gas)
135        .max(Nat::from(MIN_SUGGEST_MAX_PRIORITY_FEE_PER_GAS));
136
137    FeeEstimates {
138        max_fee_per_gas: nat_to_u256(&max_priority_fee_per_gas),
139        max_priority_fee_per_gas: nat_to_u256(&median_reward),
140    }
141}