arpa_core/utils/
mod.rs

1use chrono::Local;
2use ethers_core::{
3    types::{Address, I256, U256},
4    utils::{hex, keccak256},
5};
6use log::info;
7
8/// The threshold max change/difference (in %) at which we will ignore the fee history values
9/// under it.
10pub const EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE: i64 = 200;
11pub const OP_MAINNET_CHAIN_ID: usize = 10;
12pub const OP_GOERLI_TESTNET_CHAIN_ID: usize = 420;
13pub const OP_SEPOLIA_TESTNET_CHAIN_ID: usize = 11155420;
14pub const OP_DEVNET_CHAIN_ID: usize = 901;
15pub const BASE_MAINNET_CHAIN_ID: usize = 8453;
16pub const BASE_GOERLI_TESTNET_CHAIN_ID: usize = 84531;
17pub const BASE_SEPOLIA_TESTNET_CHAIN_ID: usize = 84532;
18pub const REDSTONE_HOLESKY_TESTNET_CHAIN_ID: usize = 17001;
19pub const REDSTONE_MAINNET_CHAIN_ID: usize = 690;
20pub const REDSTONE_GARNET_TESTNET_CHAIN_ID: usize = 17069;
21pub const LOOT_MAINNET_CHAIN_ID: usize = 5151706;
22pub const LOOT_TESTNET_CHAIN_ID: usize = 9088912;
23pub const TAIKO_KATLA_TEST_CHAIN_ID: usize = 167008;
24
25pub fn supports_eip1559(chain_id: usize) -> bool {
26    chain_id != LOOT_MAINNET_CHAIN_ID && chain_id != LOOT_TESTNET_CHAIN_ID
27}
28
29pub fn format_now_date() -> String {
30    let fmt = "%Y-%m-%d %H:%M:%S";
31    Local::now().format(fmt).to_string()
32}
33
34pub fn address_to_string(address: Address) -> String {
35    to_checksum(&address, None)
36}
37
38pub fn u256_to_vec(x: &U256) -> Vec<u8> {
39    let mut x_bytes = vec![0u8; 32];
40    x.to_big_endian(&mut x_bytes);
41    x_bytes
42}
43
44pub fn pad_to_bytes32(s: &[u8]) -> Option<[u8; 32]> {
45    let s_len = s.len();
46
47    if s_len > 32 {
48        return None;
49    }
50
51    let mut result: [u8; 32] = Default::default();
52
53    result[..s_len].clone_from_slice(s);
54
55    Some(result)
56}
57
58pub fn ser_bytes_in_hex_string<T, S>(v: &T, s: S) -> Result<S::Ok, S::Error>
59where
60    T: AsRef<[u8]>,
61    S: serde::Serializer,
62{
63    // add "0x" prefix to the hex string
64    s.serialize_str(&format!("0x{}", hex::encode(v.as_ref())))
65}
66
67pub fn ser_u256_in_dec_string<S>(v: &U256, s: S) -> Result<S::Ok, S::Error>
68where
69    S: serde::Serializer,
70{
71    s.serialize_str(&format!("{}", v))
72}
73
74/// Converts an Ethereum address to the checksum encoding
75/// Ref: <https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md>
76pub fn to_checksum(addr: &Address, chain_id: Option<u8>) -> String {
77    let prefixed_addr = match chain_id {
78        Some(chain_id) => format!("{}0x{:x}", chain_id, addr),
79        None => format!("{:x}", addr),
80    };
81    let hash = hex::encode(keccak256(prefixed_addr));
82    let hash = hash.as_bytes();
83
84    let addr_hex = hex::encode(addr.as_bytes());
85    let addr_hex = addr_hex.as_bytes();
86
87    addr_hex
88        .iter()
89        .zip(hash)
90        .fold("0x".to_owned(), |mut encoded, (addr, hash)| {
91            encoded.push(if *hash >= 56 {
92                addr.to_ascii_uppercase() as char
93            } else {
94                addr.to_ascii_lowercase() as char
95            });
96            encoded
97        })
98}
99
100/// The EIP-1559 fee estimator which is based on the work by [ethers-rs](https://github.com/gakonst/ethers-rs/blob/e0e79df7e9032e882fce4f47bcc25d87bceaec68/ethers-core/src/utils/mod.rs#L500) and [MyCrypto](https://github.com/MyCryptoHQ/MyCrypto/blob/master/src/services/ApiService/Gas/eip1559.ts)
101pub fn eip1559_gas_price_estimator(base: U256, tips: Vec<Vec<U256>>) -> (U256, U256) {
102    info!("base: {:?}", base);
103    info!("tips: {:?}", tips);
104
105    let max_priority_fee_per_gas = estimate_priority_fee(tips);
106
107    fallback_eip1559_gas_price_estimator(base, max_priority_fee_per_gas)
108}
109
110pub fn fallback_eip1559_gas_price_estimator(
111    base: U256,
112    max_priority_fee_per_gas: U256,
113) -> (U256, U256) {
114    info!("base: {:?}", base);
115    info!("max_priority_fee_per_gas: {:?}", max_priority_fee_per_gas);
116
117    let potential_max_fee = base * 12 / 10;
118
119    let max_fee_per_gas = if max_priority_fee_per_gas > potential_max_fee {
120        max_priority_fee_per_gas + potential_max_fee
121    } else {
122        potential_max_fee
123    };
124    (max_fee_per_gas, max_priority_fee_per_gas)
125}
126
127fn estimate_priority_fee(rewards: Vec<Vec<U256>>) -> U256 {
128    let mut rewards: Vec<U256> = rewards
129        .iter()
130        .map(|r| r[0])
131        .filter(|r| *r > U256::zero())
132        .collect();
133    if rewards.is_empty() {
134        return U256::zero();
135    }
136    if rewards.len() == 1 {
137        return rewards[0];
138    }
139    // Sort the rewards as we will eventually take the median.
140    rewards.sort();
141
142    // A copy of the same vector is created for convenience to calculate percentage change
143    // between subsequent fee values.
144    let mut rewards_copy = rewards.clone();
145    rewards_copy.rotate_left(1);
146
147    let mut percentage_change: Vec<I256> = rewards
148        .iter()
149        .zip(rewards_copy.iter())
150        .map(|(a, b)| {
151            let a = I256::try_from(*a).expect("priority fee overflow");
152            let b = I256::try_from(*b).expect("priority fee overflow");
153            ((b - a) * 100) / a
154        })
155        .collect();
156    percentage_change.pop();
157
158    // Fetch the max of the percentage change, and that element's index.
159    let max_change = percentage_change.iter().max().unwrap();
160    let max_change_index = percentage_change
161        .iter()
162        .position(|&c| c == *max_change)
163        .unwrap();
164
165    // If we encountered a big change in fees at a certain position, then consider only
166    // the values >= it.
167    let values = if *max_change >= EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE.into()
168        && (max_change_index >= (rewards.len() / 2))
169    {
170        rewards[max_change_index..].to_vec()
171    } else {
172        rewards
173    };
174
175    // Return the median.
176    values[values.len() / 2]
177}
178
179#[cfg(test)]
180pub mod util_tests {
181
182    use ethers_core::types::Address;
183
184    use crate::{address_to_string, format_now_date};
185
186    #[test]
187    fn test_format_now_date() {
188        let res = format_now_date();
189        println!("{:?}", res);
190    }
191
192    #[test]
193    fn test_address_format() {
194        let good_address_in_str = "0x0000000000000000000000000000000000000001";
195        let address = good_address_in_str.parse::<Address>();
196        assert!(address.is_ok());
197        let address_as_str = address_to_string(address.unwrap());
198        assert_eq!(address.unwrap(), address_as_str.parse::<Address>().unwrap());
199
200        let bad_address_in_str = "0x1";
201        let address = bad_address_in_str.parse::<Address>();
202        assert!(address.is_err());
203    }
204}