tycho_execution/encoding/evm/
utils.rs

1use std::{
2    env,
3    fs::OpenOptions,
4    io::{BufRead, BufReader, Write},
5    sync::{Arc, Mutex},
6};
7
8use alloy::{
9    primitives::{aliases::U24, Address, U256, U8},
10    providers::{
11        fillers::{BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller},
12        ProviderBuilder, RootProvider,
13    },
14    sol_types::SolValue,
15};
16use num_bigint::BigUint;
17use once_cell::sync::Lazy;
18use tokio::runtime::{Handle, Runtime};
19use tycho_common::Bytes;
20
21use crate::encoding::{errors::EncodingError, models::Swap};
22
23/// Safely converts a `Bytes` object to an `Address` object.
24///
25/// Checks the length of the `Bytes` before attempting to convert, and returns an `EncodingError`
26/// if not 20 bytes long.
27pub fn bytes_to_address(address: &Bytes) -> Result<Address, EncodingError> {
28    if address.len() == 20 {
29        Ok(Address::from_slice(address))
30    } else {
31        Err(EncodingError::InvalidInput(format!("Invalid address: {address}",)))
32    }
33}
34
35/// Converts a general `BigUint` to an EVM-specific `U256` value.
36pub fn biguint_to_u256(value: &BigUint) -> U256 {
37    let bytes = value.to_bytes_be();
38    U256::from_be_slice(&bytes)
39}
40
41/// Converts a decimal to a `U24` value. The percentage is a `f64` value between 0 and 1.
42/// MAX_UINT24 corresponds to 100%.
43pub fn percentage_to_uint24(decimal: f64) -> U24 {
44    const MAX_UINT24: u32 = 16_777_215; // 2^24 - 1
45
46    let scaled = (decimal / 1.0) * (MAX_UINT24 as f64);
47    U24::from(scaled.round())
48}
49
50/// Gets the position of a token in a list of tokens.
51pub fn get_token_position(tokens: &Vec<&Bytes>, token: &Bytes) -> Result<U8, EncodingError> {
52    let position = U8::from(
53        tokens
54            .iter()
55            .position(|t| *t == token)
56            .ok_or_else(|| {
57                EncodingError::InvalidInput(format!("Token {token} not found in tokens array"))
58            })?,
59    );
60    Ok(position)
61}
62
63/// Pads or truncates a byte slice to a fixed size array of N bytes.
64/// If input is shorter than N, it pads with zeros at the start.
65/// If input is longer than N, it truncates from the start (keeps last N bytes).
66pub fn pad_or_truncate_to_size<const N: usize>(input: &[u8]) -> Result<[u8; N], EncodingError> {
67    let mut result = [0u8; N];
68
69    if input.len() <= N {
70        // Pad with zeros at the start
71        let start = N - input.len();
72        result[start..].copy_from_slice(input);
73    } else {
74        // Truncate from the start (take last N bytes)
75        let start = input.len() - N;
76        result.copy_from_slice(&input[start..]);
77    }
78
79    Ok(result)
80}
81
82/// Extracts a static attribute from a swap.
83pub fn get_static_attribute(swap: &Swap, attribute_name: &str) -> Result<Vec<u8>, EncodingError> {
84    Ok(swap
85        .component
86        .static_attributes
87        .get(attribute_name)
88        .ok_or_else(|| EncodingError::FatalError(format!("Attribute {attribute_name} not found")))?
89        .to_vec())
90}
91
92/// Returns the current Tokio runtime handle, or creates a new one if it doesn't exist.
93/// It also returns the runtime to prevent it from being dropped before use.
94/// This is required since tycho-execution does not have a pre-existing runtime.
95pub fn get_runtime() -> Result<(Handle, Option<Arc<Runtime>>), EncodingError> {
96    match Handle::try_current() {
97        Ok(h) => Ok((h, None)),
98        Err(_) => {
99            let rt = Arc::new(Runtime::new().map_err(|_| {
100                EncodingError::FatalError("Failed to create a new tokio runtime".to_string())
101            })?);
102            Ok((rt.handle().clone(), Some(rt)))
103        }
104    }
105}
106
107pub type EVMProvider = Arc<
108    FillProvider<
109        JoinFill<
110            alloy::providers::Identity,
111            JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
112        >,
113        RootProvider,
114    >,
115>;
116
117/// Gets the client used for interacting with the EVM-compatible network.
118pub async fn get_client() -> Result<EVMProvider, EncodingError> {
119    dotenv::dotenv().ok();
120    let eth_rpc_url = env::var("RPC_URL")
121        .map_err(|_| EncodingError::FatalError("Missing RPC_URL in environment".to_string()))?;
122    let client = ProviderBuilder::new()
123        .connect(&eth_rpc_url)
124        .await
125        .map_err(|_| EncodingError::FatalError("Failed to build provider".to_string()))?;
126    Ok(Arc::new(client))
127}
128
129/// Uses prefix-length encoding to efficient encode action data.
130///
131/// Prefix-length encoding is a data encoding method where the beginning of a data segment
132/// (the "prefix") contains information about the length of the following data.
133pub fn ple_encode(action_data_array: Vec<Vec<u8>>) -> Vec<u8> {
134    let mut encoded_action_data: Vec<u8> = Vec::new();
135
136    for action_data in action_data_array {
137        let args = (encoded_action_data, action_data.len() as u16, action_data);
138        encoded_action_data = args.abi_encode_packed();
139    }
140
141    encoded_action_data
142}
143
144static CALLDATA_WRITE_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
145// Function used in tests to write calldata to a file that then is used by the corresponding
146// solidity tests.
147pub fn write_calldata_to_file(test_identifier: &str, hex_calldata: &str) {
148    let _lock = CALLDATA_WRITE_MUTEX
149        .lock()
150        .expect("Couldn't acquire lock");
151
152    let file_path = "foundry/test/assets/calldata.txt";
153    let file = OpenOptions::new()
154        .read(true)
155        .open(file_path)
156        .expect("Failed to open calldata file for reading");
157    let reader = BufReader::new(file);
158
159    let mut lines = Vec::new();
160    let mut found = false;
161    for line in reader.lines().map_while(Result::ok) {
162        let mut parts = line.splitn(2, ':'); // split at the :
163        let key = parts.next().unwrap_or("");
164        if key == test_identifier {
165            lines.push(format!("{test_identifier}:{hex_calldata}"));
166            found = true;
167        } else {
168            lines.push(line);
169        }
170    }
171
172    // If the test identifier wasn't found, append a new line
173    if !found {
174        lines.push(format!("{test_identifier}:{hex_calldata}"));
175    }
176
177    // Write the updated contents back to the file
178    let mut file = OpenOptions::new()
179        .write(true)
180        .truncate(true)
181        .open(file_path)
182        .expect("Failed to open calldata file for writing");
183
184    for line in lines {
185        writeln!(file, "{line}").expect("Failed to write calldata");
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_pad_or_truncate_to_size() {
195        // Test padding
196        let input = hex::decode("0110").unwrap();
197        let result = pad_or_truncate_to_size::<3>(&input).unwrap();
198        assert_eq!(hex::encode(result), "000110");
199
200        // Test truncation
201        let input_long = hex::decode("00800000").unwrap();
202        let result_truncated = pad_or_truncate_to_size::<3>(&input_long).unwrap();
203        assert_eq!(hex::encode(result_truncated), "800000");
204    }
205}