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, evm::constants::ROUTER_ETH_ADDRESS, models::Swap};
22
23/// Converts `Address::ZERO` (protocol-native ETH marker) to the
24/// `ETH_ADDRESS` marker (0xEeee…) used by the TychoRouter. Non-zero
25/// addresses pass through unchanged.
26pub fn convert_to_router_token(addr: Address) -> Address {
27    if addr == Address::ZERO {
28        Address::from_slice(&ROUTER_ETH_ADDRESS)
29    } else {
30        addr
31    }
32}
33
34/// Safely converts a `Bytes` object to an `Address` object.
35///
36/// Checks the length of the `Bytes` before attempting to convert, and returns an `EncodingError`
37/// if not 20 bytes long.
38pub fn bytes_to_address(address: &Bytes) -> Result<Address, EncodingError> {
39    if address.len() == 20 {
40        Ok(Address::from_slice(address))
41    } else {
42        Err(EncodingError::InvalidInput(format!("Invalid address: {address}",)))
43    }
44}
45
46/// Converts a general `BigUint` to an EVM-specific `U256` value.
47pub fn biguint_to_u256(value: &BigUint) -> U256 {
48    let bytes = value.to_bytes_be();
49    U256::from_be_slice(&bytes)
50}
51
52/// Converts a decimal to a `U24` value. The percentage is a `f64` value between 0 and 1.
53/// MAX_UINT24 corresponds to 100%.
54pub(crate) fn percentage_to_uint24(decimal: f64) -> U24 {
55    const MAX_UINT24: u32 = 16_777_215; // 2^24 - 1
56
57    let scaled = (decimal / 1.0) * (MAX_UINT24 as f64);
58    U24::from(scaled.round())
59}
60
61/// Gets the position of a token in a list of tokens.
62pub(crate) fn get_token_position(tokens: &Vec<&Bytes>, token: &Bytes) -> Result<U8, EncodingError> {
63    let position = U8::from(
64        tokens
65            .iter()
66            .position(|t| *t == token)
67            .ok_or_else(|| {
68                EncodingError::InvalidInput(format!("Token {token} not found in tokens array"))
69            })?,
70    );
71    Ok(position)
72}
73
74/// Pads or truncates a byte slice to a fixed size array of N bytes.
75/// If input is shorter than N, it pads with zeros at the start.
76/// If input is longer than N, it truncates from the start (keeps last N bytes).
77pub(crate) fn pad_or_truncate_to_size<const N: usize>(
78    input: &[u8],
79) -> Result<[u8; N], EncodingError> {
80    let mut result = [0u8; N];
81
82    if input.len() <= N {
83        // Pad with zeros at the start
84        let start = N - input.len();
85        result[start..].copy_from_slice(input);
86    } else {
87        // Truncate from the start (take last N bytes)
88        let start = input.len() - N;
89        result.copy_from_slice(&input[start..]);
90    }
91
92    Ok(result)
93}
94
95/// Extracts a static attribute from a swap.
96pub(crate) fn get_static_attribute(
97    swap: &Swap,
98    attribute_name: &str,
99) -> Result<Vec<u8>, EncodingError> {
100    Ok(swap
101        .component()
102        .static_attributes
103        .get(attribute_name)
104        .ok_or_else(|| EncodingError::FatalError(format!("Attribute {attribute_name} not found")))?
105        .to_vec())
106}
107
108/// A tokio `Runtime` wrapped in `Arc` that safely drops from async contexts.
109///
110/// If dropped while a tokio runtime is active on the current thread, ensures
111/// the actual runtime shutdown happens on a background OS thread, avoiding the
112/// "cannot drop a runtime in a context where blocking is not allowed" panic.
113#[derive(Clone)]
114pub(crate) struct SafeRuntime(Option<Arc<Runtime>>);
115
116impl Drop for SafeRuntime {
117    fn drop(&mut self) {
118        if let Some(rt) = self.0.take() {
119            if tokio::runtime::Handle::try_current().is_ok() {
120                std::thread::spawn(move || drop(rt));
121            }
122        }
123    }
124}
125
126/// Creates a dedicated multi-thread tokio runtime for encoding operations.
127///
128/// Always creates a new runtime rather than reusing the caller's, so that I/O
129/// futures are driven by dedicated worker threads regardless of the caller's
130/// runtime flavor (including current-thread runtimes like actix-web workers).
131///
132/// Returns the runtime handle and a [`SafeRuntime`] that can be dropped safely
133/// from any context.
134pub(crate) fn create_encoding_runtime() -> Result<(Handle, SafeRuntime), EncodingError> {
135    let rt = Arc::new(
136        tokio::runtime::Builder::new_multi_thread()
137            .worker_threads(1)
138            .enable_all()
139            .build()
140            .map_err(|_| {
141                EncodingError::FatalError("Failed to create encoding runtime".to_string())
142            })?,
143    );
144    let handle = rt.handle().clone();
145    Ok((handle, SafeRuntime(Some(rt))))
146}
147
148/// Runs a closure on a fresh OS thread, blocking the caller until it completes.
149///
150/// Unlike `tokio::task::block_in_place`, this works on any runtime flavor
151/// (including current-thread) because the spawned thread has no tokio context.
152/// Typical usage: `on_blocking_thread(|| handle.block_on(some_future))`.
153pub(crate) fn on_blocking_thread<F, T>(f: F) -> Result<T, EncodingError>
154where
155    F: FnOnce() -> T + Send,
156    T: Send,
157{
158    std::thread::scope(|s| {
159        s.spawn(f)
160            .join()
161            .map_err(|_| EncodingError::FatalError("blocking thread panicked".to_string()))
162    })
163}
164
165pub(crate) type EVMProvider = Arc<
166    FillProvider<
167        JoinFill<
168            alloy::providers::Identity,
169            JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
170        >,
171        RootProvider,
172    >,
173>;
174
175/// Gets the client used for interacting with the EVM-compatible network.
176pub(crate) async fn get_client() -> Result<EVMProvider, EncodingError> {
177    dotenvy::dotenv().ok();
178    let eth_rpc_url = env::var("RPC_URL")
179        .map_err(|_| EncodingError::FatalError("Missing RPC_URL in environment".to_string()))?;
180    let client = ProviderBuilder::new()
181        .connect(&eth_rpc_url)
182        .await
183        .map_err(|_| EncodingError::FatalError("Failed to build provider".to_string()))?;
184    Ok(Arc::new(client))
185}
186
187/// Uses prefix-length encoding to efficient encode action data.
188///
189/// Prefix-length encoding is a data encoding method where the beginning of a data segment
190/// (the "prefix") contains information about the length of the following data.
191pub(crate) fn ple_encode(action_data_array: Vec<Vec<u8>>) -> Vec<u8> {
192    let mut encoded_action_data: Vec<u8> = Vec::new();
193
194    for action_data in action_data_array {
195        let args = (encoded_action_data, action_data.len() as u16, action_data);
196        encoded_action_data = args.abi_encode_packed();
197    }
198
199    encoded_action_data
200}
201
202static CALLDATA_WRITE_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
203// Function used in tests to write calldata to a file that then is used by the corresponding
204// solidity tests.
205pub fn write_calldata_to_file(test_identifier: &str, hex_calldata: &str) {
206    let _lock = CALLDATA_WRITE_MUTEX
207        .lock()
208        .expect("Couldn't acquire lock");
209
210    let file_path = "contracts/test/assets/calldata.txt";
211    let file = OpenOptions::new()
212        .read(true)
213        .open(file_path)
214        .expect("Failed to open calldata file for reading");
215    let reader = BufReader::new(file);
216
217    let mut lines = Vec::new();
218    let mut found = false;
219    for line in reader.lines().map_while(Result::ok) {
220        let mut parts = line.splitn(2, ':'); // split at the :
221        let key = parts.next().unwrap_or("");
222        if key == test_identifier {
223            lines.push(format!("{test_identifier}:{hex_calldata}"));
224            found = true;
225        } else {
226            lines.push(line);
227        }
228    }
229
230    // If the test identifier wasn't found, append a new line
231    if !found {
232        lines.push(format!("{test_identifier}:{hex_calldata}"));
233    }
234
235    // Write the updated contents back to the file
236    let mut file = OpenOptions::new()
237        .write(true)
238        .truncate(true)
239        .open(file_path)
240        .expect("Failed to open calldata file for writing");
241
242    for line in lines {
243        writeln!(file, "{line}").expect("Failed to write calldata");
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_pad_or_truncate_to_size() {
253        // Test padding
254        let input = hex::decode("0110").unwrap();
255        let result = pad_or_truncate_to_size::<3>(&input).unwrap();
256        assert_eq!(hex::encode(result), "000110");
257
258        // Test truncation
259        let input_long = hex::decode("00800000").unwrap();
260        let result_truncated = pad_or_truncate_to_size::<3>(&input_long).unwrap();
261        assert_eq!(hex::encode(result_truncated), "800000");
262    }
263}