Skip to main content

tycho_execution/encoding/evm/
utils.rs

1use std::{
2    env,
3    fs::OpenOptions,
4    io::{BufRead, BufReader, Write},
5    sync::{
6        atomic::{AtomicUsize, Ordering},
7        Arc, Mutex,
8    },
9};
10
11use alloy::{
12    primitives::{aliases::U24, Address, U256, U8},
13    providers::{
14        fillers::{BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller},
15        ProviderBuilder, RootProvider,
16    },
17    sol_types::SolValue,
18};
19use num_bigint::BigUint;
20use once_cell::sync::Lazy;
21use tokio::runtime::{Handle, Runtime};
22use tycho_common::Bytes;
23
24use crate::encoding::{errors::EncodingError, evm::constants::ROUTER_ETH_ADDRESS, models::Swap};
25
26/// Converts `Address::ZERO` (protocol-native ETH marker) to the
27/// `ETH_ADDRESS` marker (0xEeee…) used by the TychoRouterV3. Non-zero
28/// addresses pass through unchanged.
29pub fn convert_to_router_token(addr: Address) -> Address {
30    if addr == Address::ZERO {
31        Address::from_slice(&ROUTER_ETH_ADDRESS)
32    } else {
33        addr
34    }
35}
36
37/// Safely converts a `Bytes` object to an `Address` object.
38///
39/// Checks the length of the `Bytes` before attempting to convert, and returns an `EncodingError`
40/// if not 20 bytes long.
41pub fn bytes_to_address(address: &Bytes) -> Result<Address, EncodingError> {
42    if address.len() == 20 {
43        Ok(Address::from_slice(address))
44    } else {
45        Err(EncodingError::InvalidInput(format!("Invalid address: {address}",)))
46    }
47}
48
49/// Converts a general `BigUint` to an EVM-specific `U256` value.
50pub fn biguint_to_u256(value: &BigUint) -> U256 {
51    let bytes = value.to_bytes_be();
52    U256::from_be_slice(&bytes)
53}
54
55/// Converts a decimal to a `U24` value. The percentage is a `f64` value between 0 and 1.
56/// MAX_UINT24 corresponds to 100%.
57pub(crate) fn percentage_to_uint24(decimal: f64) -> U24 {
58    const MAX_UINT24: u32 = 16_777_215; // 2^24 - 1
59
60    let scaled = (decimal / 1.0) * (MAX_UINT24 as f64);
61    U24::from(scaled.round())
62}
63
64/// Gets the position of a token in a list of tokens.
65pub(crate) fn get_token_position(tokens: &Vec<&Bytes>, token: &Bytes) -> Result<U8, EncodingError> {
66    let position = U8::from(
67        tokens
68            .iter()
69            .position(|t| *t == token)
70            .ok_or_else(|| {
71                EncodingError::InvalidInput(format!("Token {token} not found in tokens array"))
72            })?,
73    );
74    Ok(position)
75}
76
77/// Pads or truncates a byte slice to a fixed size array of N bytes.
78/// If input is shorter than N, it pads with zeros at the start.
79/// If input is longer than N, it truncates from the start (keeps last N bytes).
80pub(crate) fn pad_or_truncate_to_size<const N: usize>(
81    input: &[u8],
82) -> Result<[u8; N], EncodingError> {
83    let mut result = [0u8; N];
84
85    if input.len() <= N {
86        // Pad with zeros at the start
87        let start = N - input.len();
88        result[start..].copy_from_slice(input);
89    } else {
90        // Truncate from the start (take last N bytes)
91        let start = input.len() - N;
92        result.copy_from_slice(&input[start..]);
93    }
94
95    Ok(result)
96}
97
98/// Extracts a static attribute from a swap.
99pub(crate) fn get_static_attribute(
100    swap: &Swap,
101    attribute_name: &str,
102) -> Result<Vec<u8>, EncodingError> {
103    Ok(swap
104        .component()
105        .static_attributes
106        .get(attribute_name)
107        .ok_or_else(|| EncodingError::FatalError(format!("Attribute {attribute_name} not found")))?
108        .to_vec())
109}
110
111/// A tokio `Runtime` wrapped in `Arc` that safely drops from async contexts.
112///
113/// If dropped while a tokio runtime is active on the current thread, ensures
114/// the actual runtime shutdown happens on a background OS thread, avoiding the
115/// "cannot drop a runtime in a context where blocking is not allowed" panic.
116#[derive(Clone)]
117pub(crate) struct SafeRuntime(Option<Arc<Runtime>>);
118
119impl Drop for SafeRuntime {
120    fn drop(&mut self) {
121        if let Some(rt) = self.0.take() {
122            if tokio::runtime::Handle::try_current().is_ok() {
123                std::thread::spawn(move || drop(rt));
124            }
125        }
126    }
127}
128
129/// Creates a dedicated multi-thread tokio runtime for encoding operations.
130///
131/// Always creates a new runtime rather than reusing the caller's, so that I/O
132/// futures are driven by dedicated worker threads regardless of the caller's
133/// runtime flavor (including current-thread runtimes like actix-web workers).
134///
135/// Returns the runtime handle and a [`SafeRuntime`] that can be dropped safely
136/// from any context.
137pub(crate) fn create_encoding_runtime() -> Result<(Handle, SafeRuntime), EncodingError> {
138    let rt = Arc::new(
139        tokio::runtime::Builder::new_multi_thread()
140            .worker_threads(1)
141            .enable_all()
142            .build()
143            .map_err(|_| {
144                EncodingError::FatalError("Failed to create encoding runtime".to_string())
145            })?,
146    );
147    let handle = rt.handle().clone();
148    Ok((handle, SafeRuntime(Some(rt))))
149}
150
151/// Extracts the human-readable message from a panic payload, if it carries one.
152fn panic_message(payload: &(dyn std::any::Any + Send)) -> &str {
153    if let Some(message) = payload.downcast_ref::<&str>() {
154        message
155    } else if let Some(message) = payload.downcast_ref::<String>() {
156        message
157    } else {
158        "non-string panic payload"
159    }
160}
161
162/// Runs a closure on a fresh OS thread, blocking the caller until it completes.
163///
164/// Unlike `tokio::task::block_in_place`, this works on any runtime flavor
165/// (including current-thread) because the spawned thread has no tokio context.
166/// Typical usage: `on_blocking_thread(|| handle.block_on(some_future))`.
167pub(crate) fn on_blocking_thread<F, T>(f: F) -> Result<T, EncodingError>
168where
169    F: FnOnce() -> T + Send,
170    T: Send,
171{
172    std::thread::scope(|s| {
173        s.spawn(f).join().map_err(|payload| {
174            EncodingError::FatalError(format!(
175                "blocking thread panicked: {}",
176                panic_message(&*payload)
177            ))
178        })
179    })
180}
181
182/// Upper bound on the OS threads one `map_on_threads` call runs at a time.
183///
184/// The threads block on RFQ network round trips, so the cap bounds peak memory (each thread
185/// reserves stack space) while still overlapping the waits.
186const MAX_ENCODING_THREADS: usize = 32;
187
188/// Runs `f` on every item on up to [`MAX_ENCODING_THREADS`] OS threads, and returns the results
189/// in input order.
190///
191/// An RFQ encoder blocks on a network round trip for its signed quote, so running the items at
192/// the same time bounds the total wait by the slowest item instead of the sum of all items.
193///
194/// A single item runs on the calling thread. Any item's error is returned; earlier items win.
195pub(crate) fn map_on_threads<T, R, F>(items: &[T], f: F) -> Result<Vec<R>, EncodingError>
196where
197    T: Sync,
198    R: Send,
199    F: Fn(&T) -> Result<R, EncodingError> + Sync,
200{
201    if let [item] = items {
202        return Ok(vec![f(item)?]);
203    }
204    let next_index = AtomicUsize::new(0);
205    let workers = items.len().min(MAX_ENCODING_THREADS);
206    std::thread::scope(|scope| {
207        let mut handles = Vec::with_capacity(workers);
208        for _ in 0..workers {
209            handles.push(scope.spawn(|| {
210                let mut worker_results = Vec::new();
211                loop {
212                    let index = next_index.fetch_add(1, Ordering::Relaxed);
213                    let Some(item) = items.get(index) else {
214                        return worker_results;
215                    };
216                    worker_results.push((index, f(item)));
217                }
218            }));
219        }
220        let mut results: Vec<Option<R>> = Vec::new();
221        results.resize_with(items.len(), || None);
222        let mut first_error: Option<(usize, EncodingError)> = None;
223        for handle in handles {
224            let worker_results = handle.join().map_err(|payload| {
225                EncodingError::FatalError(format!(
226                    "encoding thread panicked: {}",
227                    panic_message(&*payload)
228                ))
229            })?;
230            for (index, result) in worker_results {
231                match result {
232                    Ok(value) => results[index] = Some(value),
233                    Err(error) => {
234                        if first_error
235                            .as_ref()
236                            .is_none_or(|(first_index, _)| index < *first_index)
237                        {
238                            first_error = Some((index, error));
239                        }
240                    }
241                }
242            }
243        }
244        if let Some((_, error)) = first_error {
245            return Err(error);
246        }
247        let mut ordered = Vec::with_capacity(items.len());
248        for result in results {
249            ordered.push(result.ok_or_else(|| {
250                EncodingError::FatalError("encoding thread dropped a result".to_string())
251            })?);
252        }
253        Ok(ordered)
254    })
255}
256
257pub(crate) type EVMProvider = Arc<
258    FillProvider<
259        JoinFill<
260            alloy::providers::Identity,
261            JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
262        >,
263        RootProvider,
264    >,
265>;
266
267/// Gets the client used for interacting with the EVM-compatible network.
268pub(crate) async fn get_client() -> Result<EVMProvider, EncodingError> {
269    dotenvy::dotenv().ok();
270    let eth_rpc_url = env::var("RPC_URL")
271        .map_err(|_| EncodingError::FatalError("Missing RPC_URL in environment".to_string()))?;
272    let client = ProviderBuilder::new()
273        .connect(&eth_rpc_url)
274        .await
275        .map_err(|_| EncodingError::FatalError("Failed to build provider".to_string()))?;
276    Ok(Arc::new(client))
277}
278
279/// Uses prefix-length encoding to efficient encode action data.
280///
281/// Prefix-length encoding is a data encoding method where the beginning of a data segment
282/// (the "prefix") contains information about the length of the following data.
283pub(crate) fn ple_encode(action_data_array: Vec<Vec<u8>>) -> Vec<u8> {
284    let mut encoded_action_data: Vec<u8> = Vec::new();
285
286    for action_data in action_data_array {
287        let args = (encoded_action_data, action_data.len() as u16, action_data);
288        encoded_action_data = args.abi_encode_packed();
289    }
290
291    encoded_action_data
292}
293
294static CALLDATA_WRITE_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
295// Function used in tests to write calldata to a file that then is used by the corresponding
296// solidity tests.
297pub fn write_calldata_to_file(test_identifier: &str, hex_calldata: &str) {
298    let _lock = CALLDATA_WRITE_MUTEX
299        .lock()
300        .expect("Couldn't acquire lock");
301
302    let file_path = "contracts/test/assets/calldata.txt";
303    let file = OpenOptions::new()
304        .read(true)
305        .open(file_path)
306        .expect("Failed to open calldata file for reading");
307    let reader = BufReader::new(file);
308
309    let mut lines = Vec::new();
310    let mut found = false;
311    for line in reader.lines().map_while(Result::ok) {
312        let mut parts = line.splitn(2, ':'); // split at the :
313        let key = parts.next().unwrap_or("");
314        if key == test_identifier {
315            lines.push(format!("{test_identifier}:{hex_calldata}"));
316            found = true;
317        } else {
318            lines.push(line);
319        }
320    }
321
322    // If the test identifier wasn't found, append a new line
323    if !found {
324        lines.push(format!("{test_identifier}:{hex_calldata}"));
325    }
326
327    // Write the updated contents back to the file
328    let mut file = OpenOptions::new()
329        .write(true)
330        .truncate(true)
331        .open(file_path)
332        .expect("Failed to open calldata file for writing");
333
334    for line in lines {
335        writeln!(file, "{line}").expect("Failed to write calldata");
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn test_map_on_threads_keeps_input_order_above_the_thread_cap() {
345        let items: Vec<usize> = (0..(MAX_ENCODING_THREADS * 3 + 1)).collect();
346
347        let results = map_on_threads(&items, |item| Ok(*item * 2)).unwrap();
348
349        let expected: Vec<usize> = items
350            .iter()
351            .map(|item| item * 2)
352            .collect();
353        assert_eq!(results, expected);
354    }
355
356    #[test]
357    fn test_map_on_threads_returns_the_earliest_error() {
358        let items: Vec<usize> = (0..(MAX_ENCODING_THREADS * 2)).collect();
359
360        let result: Result<Vec<usize>, EncodingError> = map_on_threads(&items, |item| {
361            if *item >= 3 {
362                Err(EncodingError::InvalidInput(format!("item {item} is broken")))
363            } else {
364                Ok(*item)
365            }
366        });
367
368        let Err(EncodingError::InvalidInput(message)) = result else {
369            panic!("expected an InvalidInput error, got {result:?}");
370        };
371        assert_eq!(message, "item 3 is broken");
372    }
373
374    #[test]
375    fn test_map_on_threads_reports_the_panic_message() {
376        let items = vec![1, 2];
377
378        let result: Result<Vec<i32>, EncodingError> = map_on_threads(&items, |item| {
379            assert_ne!(*item, 2, "item 2 is broken");
380            Ok(*item)
381        });
382
383        let Err(EncodingError::FatalError(message)) = result else {
384            panic!("expected a FatalError, got {result:?}");
385        };
386        assert!(message.contains("item 2 is broken"), "{message}");
387    }
388
389    #[test]
390    fn test_pad_or_truncate_to_size() {
391        // Test padding
392        let input = hex::decode("0110").unwrap();
393        let result = pad_or_truncate_to_size::<3>(&input).unwrap();
394        assert_eq!(hex::encode(result), "000110");
395
396        // Test truncation
397        let input_long = hex::decode("00800000").unwrap();
398        let result_truncated = pad_or_truncate_to_size::<3>(&input_long).unwrap();
399        assert_eq!(hex::encode(result_truncated), "800000");
400    }
401}