Skip to main content

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(crate) 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(crate) 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(crate) fn pad_or_truncate_to_size<const N: usize>(
67    input: &[u8],
68) -> Result<[u8; N], EncodingError> {
69    let mut result = [0u8; N];
70
71    if input.len() <= N {
72        // Pad with zeros at the start
73        let start = N - input.len();
74        result[start..].copy_from_slice(input);
75    } else {
76        // Truncate from the start (take last N bytes)
77        let start = input.len() - N;
78        result.copy_from_slice(&input[start..]);
79    }
80
81    Ok(result)
82}
83
84/// Extracts a static attribute from a swap.
85pub(crate) fn get_static_attribute(
86    swap: &Swap,
87    attribute_name: &str,
88) -> Result<Vec<u8>, EncodingError> {
89    Ok(swap
90        .component()
91        .static_attributes
92        .get(attribute_name)
93        .ok_or_else(|| EncodingError::FatalError(format!("Attribute {attribute_name} not found")))?
94        .to_vec())
95}
96
97/// A tokio `Runtime` wrapped in `Arc` that safely drops from async contexts.
98///
99/// If dropped while a tokio runtime is active on the current thread, ensures
100/// the actual runtime shutdown happens on a background OS thread, avoiding the
101/// "cannot drop a runtime in a context where blocking is not allowed" panic.
102#[derive(Clone)]
103pub(crate) struct SafeRuntime(Option<Arc<Runtime>>);
104
105impl Drop for SafeRuntime {
106    fn drop(&mut self) {
107        if let Some(rt) = self.0.take() {
108            if tokio::runtime::Handle::try_current().is_ok() {
109                std::thread::spawn(move || drop(rt));
110            }
111        }
112    }
113}
114
115/// Creates a dedicated multi-thread tokio runtime for encoding operations.
116///
117/// Always creates a new runtime rather than reusing the caller's, so that I/O
118/// futures are driven by dedicated worker threads regardless of the caller's
119/// runtime flavor (including current-thread runtimes like actix-web workers).
120///
121/// Returns the runtime handle and a [`SafeRuntime`] that can be dropped safely
122/// from any context.
123pub(crate) fn create_encoding_runtime() -> Result<(Handle, SafeRuntime), EncodingError> {
124    let rt = Arc::new(
125        tokio::runtime::Builder::new_multi_thread()
126            .worker_threads(1)
127            .enable_all()
128            .build()
129            .map_err(|_| {
130                EncodingError::FatalError("Failed to create encoding runtime".to_string())
131            })?,
132    );
133    let handle = rt.handle().clone();
134    Ok((handle, SafeRuntime(Some(rt))))
135}
136
137/// Runs a closure on a fresh OS thread, blocking the caller until it completes.
138///
139/// Unlike `tokio::task::block_in_place`, this works on any runtime flavor
140/// (including current-thread) because the spawned thread has no tokio context.
141/// Typical usage: `on_blocking_thread(|| handle.block_on(some_future))`.
142pub(crate) fn on_blocking_thread<F, T>(f: F) -> Result<T, EncodingError>
143where
144    F: FnOnce() -> T + Send,
145    T: Send,
146{
147    std::thread::scope(|s| {
148        s.spawn(f)
149            .join()
150            .map_err(|_| EncodingError::FatalError("blocking thread panicked".to_string()))
151    })
152}
153
154pub(crate) type EVMProvider = Arc<
155    FillProvider<
156        JoinFill<
157            alloy::providers::Identity,
158            JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
159        >,
160        RootProvider,
161    >,
162>;
163
164/// Gets the client used for interacting with the EVM-compatible network.
165pub(crate) async fn get_client() -> Result<EVMProvider, EncodingError> {
166    dotenvy::dotenv().ok();
167    let eth_rpc_url = env::var("RPC_URL")
168        .map_err(|_| EncodingError::FatalError("Missing RPC_URL in environment".to_string()))?;
169    let client = ProviderBuilder::new()
170        .connect(&eth_rpc_url)
171        .await
172        .map_err(|_| EncodingError::FatalError("Failed to build provider".to_string()))?;
173    Ok(Arc::new(client))
174}
175
176/// Uses prefix-length encoding to efficient encode action data.
177///
178/// Prefix-length encoding is a data encoding method where the beginning of a data segment
179/// (the "prefix") contains information about the length of the following data.
180pub(crate) fn ple_encode(action_data_array: Vec<Vec<u8>>) -> Vec<u8> {
181    let mut encoded_action_data: Vec<u8> = Vec::new();
182
183    for action_data in action_data_array {
184        let args = (encoded_action_data, action_data.len() as u16, action_data);
185        encoded_action_data = args.abi_encode_packed();
186    }
187
188    encoded_action_data
189}
190
191static CALLDATA_WRITE_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
192// Function used in tests to write calldata to a file that then is used by the corresponding
193// solidity tests.
194pub fn write_calldata_to_file(test_identifier: &str, hex_calldata: &str) {
195    let _lock = CALLDATA_WRITE_MUTEX
196        .lock()
197        .expect("Couldn't acquire lock");
198
199    let file_path = "foundry/test/assets/calldata.txt";
200    let file = OpenOptions::new()
201        .read(true)
202        .open(file_path)
203        .expect("Failed to open calldata file for reading");
204    let reader = BufReader::new(file);
205
206    let mut lines = Vec::new();
207    let mut found = false;
208    for line in reader.lines().map_while(Result::ok) {
209        let mut parts = line.splitn(2, ':'); // split at the :
210        let key = parts.next().unwrap_or("");
211        if key == test_identifier {
212            lines.push(format!("{test_identifier}:{hex_calldata}"));
213            found = true;
214        } else {
215            lines.push(line);
216        }
217    }
218
219    // If the test identifier wasn't found, append a new line
220    if !found {
221        lines.push(format!("{test_identifier}:{hex_calldata}"));
222    }
223
224    // Write the updated contents back to the file
225    let mut file = OpenOptions::new()
226        .write(true)
227        .truncate(true)
228        .open(file_path)
229        .expect("Failed to open calldata file for writing");
230
231    for line in lines {
232        writeln!(file, "{line}").expect("Failed to write calldata");
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_pad_or_truncate_to_size() {
242        // Test padding
243        let input = hex::decode("0110").unwrap();
244        let result = pad_or_truncate_to_size::<3>(&input).unwrap();
245        assert_eq!(hex::encode(result), "000110");
246
247        // Test truncation
248        let input_long = hex::decode("00800000").unwrap();
249        let result_truncated = pad_or_truncate_to_size::<3>(&input_long).unwrap();
250        assert_eq!(hex::encode(result_truncated), "800000");
251    }
252}