Skip to main content

tycho_simulation/evm/protocol/vm/
utils.rs

1use std::{collections::HashMap, env, fmt::Debug, str::FromStr};
2
3use alloy::{
4    primitives::{Address, Bytes, Keccak256, U256},
5    providers::{Provider, ProviderBuilder},
6    sol_types::SolValue,
7    transports::{RpcError, TransportErrorKind},
8};
9use hex::FromHex;
10use num_bigint::BigInt;
11use revm::{
12    state::{AccountInfo, Bytecode},
13    DatabaseRef,
14};
15use serde_json::Value;
16use tycho_common::simulation::errors::SimulationError;
17
18use crate::evm::{
19    engine_db::engine_db_interface::EngineDatabaseInterface,
20    simulation::{SimulationEngine, SimulationEngineError, SimulationParameters},
21    ContractCompiler, SlotId,
22};
23
24pub(crate) fn coerce_error(
25    err: &SimulationEngineError,
26    pool_state: &str,
27    gas_limit: Option<u64>,
28) -> SimulationError {
29    match err {
30        // Check for revert situation (if error message starts with "0x")
31        SimulationEngineError::TransactionError { ref data, ref gas_used }
32            if data.starts_with("0x") =>
33        {
34            let reason = parse_solidity_error_message(data);
35            let err = SimulationEngineError::TransactionError {
36                data: format!("Revert! Reason: {reason}"),
37                gas_used: *gas_used,
38            };
39
40            // Check if we are running out of gas
41            if let (Some(gas_limit), Some(gas_used)) = (gas_limit, gas_used) {
42                // if we used up 97% or more issue a OutOfGas error.
43                let usage = *gas_used as f64 / gas_limit as f64;
44                if usage >= 0.97 {
45                    return SimulationError::InvalidInput(
46                        format!(
47                            "SimulationError: Likely out-of-gas. Used: {:.2}% of gas limit. \
48                            Original error: {}. \
49                            Pool state: {}",
50                            usage * 100.0,
51                            err,
52                            pool_state,
53                        ),
54                        None,
55                    );
56                }
57            }
58            SimulationError::FatalError(format!("Simulation reverted for unknown reason: {reason}"))
59        }
60        // Check if "OutOfGas" is part of the error message
61        SimulationEngineError::TransactionError { ref data, ref gas_used }
62            if data.contains("OutOfGas") =>
63        {
64            let usage_msg = if let (Some(gas_limit), Some(gas_used)) = (gas_limit, gas_used) {
65                let usage = *gas_used as f64 / gas_limit as f64;
66                format!("Used: {:.2}% of gas limit. ", usage * 100.0)
67            } else {
68                String::new()
69            };
70
71            SimulationError::InvalidInput(
72                format!(
73                    "SimulationError: out-of-gas. {usage_msg} Original error: {data}. Pool state: {pool_state}"
74                ),
75                None,
76            )
77        }
78        SimulationEngineError::TransactionError { ref data, .. } => {
79            SimulationError::FatalError(format!("TransactionError: {data}"))
80        }
81        SimulationEngineError::StorageError(message) => {
82            SimulationError::RecoverableError(message.clone())
83        }
84        _ => SimulationError::FatalError(err.clone().to_string()), /* Otherwise return the
85                                                                    * original error */
86    }
87}
88
89fn parse_solidity_error_message(data: &str) -> String {
90    // 10 for "0x" + 8 hex chars error signature
91    if data.len() >= 10 {
92        let data_bytes = match Vec::from_hex(&data[2..]) {
93            Ok(bytes) => bytes,
94            Err(_) => return format!("Failed to decode: {data}"),
95        };
96
97        // Check for specific error selectors:
98        // Solidity Error(string) signature: 0x08c379a0
99        if data_bytes.starts_with(&[0x08, 0xc3, 0x79, 0xa0]) {
100            if let Ok(decoded) = String::abi_decode(&data_bytes[4..]) {
101                return decoded;
102            }
103
104            // Solidity Panic(uint256) signature: 0x4e487b71
105        } else if data_bytes.starts_with(&[0x4e, 0x48, 0x7b, 0x71]) {
106            if let Ok(decoded) = U256::abi_decode(&data_bytes[4..]) {
107                let panic_codes = get_solidity_panic_codes();
108                return panic_codes
109                    .get(&decoded.as_limbs()[0])
110                    .cloned()
111                    .unwrap_or_else(|| format!("Panic({decoded})"));
112            }
113        }
114
115        // Try decoding as a string (old Solidity revert case)
116        if let Ok(decoded) = String::abi_decode(&data_bytes) {
117            return decoded;
118        }
119
120        // Custom error, try to decode string again with offset
121        if let Ok(decoded) = String::abi_decode(&data_bytes[4..]) {
122            return decoded;
123        }
124    }
125    // Fallback if no decoding succeeded
126    format!("Failed to decode: {data}")
127}
128
129/// Get storage slot index of a value stored at a certain key in a mapping
130///
131/// # Arguments
132///
133/// * `key`: Key in a mapping. Can be any H160 value (such as an address).
134/// * `mapping_slot`: An `U256` representing the storage slot at which the mapping itself is stored.
135///   See the examples for more explanation.
136/// * `compiler`: The compiler with which the target contract was compiled. Solidity and Vyper
137///   handle maps differently.
138///
139/// # Returns
140///
141/// An `U256` representing the  index of a storage slot where the value at the given
142/// key is stored.
143///
144/// # Examples
145///
146/// If a mapping is declared as a first variable in Solidity code, its storage slot
147/// is 0 (e.g. `balances` in our mocked ERC20 contract). Here's how to compute
148/// a storage slot where balance of a given account is stored:
149///
150/// ```
151/// use alloy::primitives::{U256, Address};
152/// use tycho_simulation::evm::ContractCompiler;
153/// use tycho_simulation::evm::protocol::vm::utils::get_storage_slot_index_at_key;
154/// let address: Address = "0xC63135E4bF73F637AF616DFd64cf701866BB2628".parse().expect("Invalid address");
155/// get_storage_slot_index_at_key(address, U256::from(0), ContractCompiler::Solidity);
156/// ```
157///
158/// For nested mappings, we need to apply the function twice. An example of this is
159/// `allowances` in ERC20. It is a mapping of form:
160/// `HashMap<Owner, HashMap<Spender, U256>>`. In our mocked ERC20 contract, `allowances`
161/// is a second variable, so it is stored at slot 1. Here's how to get a storage slot
162/// where an allowance of `address_spender` to spend `address_owner`'s money is stored:
163///
164/// ```
165/// use alloy::primitives::{U256, Address};
166/// use tycho_simulation::evm::ContractCompiler;
167/// use tycho_simulation::evm::protocol::vm::utils::get_storage_slot_index_at_key;
168/// let address_spender: Address = "0xC63135E4bF73F637AF616DFd64cf701866BB2628".parse().expect("Invalid address");
169/// let address_owner: Address = "0x6F4Feb566b0f29e2edC231aDF88Fe7e1169D7c05".parse().expect("Invalid address");
170/// get_storage_slot_index_at_key(address_spender, get_storage_slot_index_at_key(address_owner, U256::from(1), ContractCompiler::Solidity), ContractCompiler::Solidity);
171/// ```
172///
173/// # See Also
174///
175/// [Solidity Storage Layout documentation](https://docs.soliditylang.org/en/v0.8.13/internals/layout_in_storage.html#mappings-and-dynamic-arrays)
176pub fn get_storage_slot_index_at_key(
177    key: Address,
178    mapping_slot: SlotId,
179    compiler: ContractCompiler,
180) -> SlotId {
181    let mut key_bytes = key.as_slice().to_vec();
182    if key_bytes.len() < 32 {
183        let padding = vec![0u8; 32 - key_bytes.len()];
184        key_bytes.splice(0..0, padding); // Prepend zeros to the start
185    }
186
187    let mapping_slot_bytes: [u8; 32] = mapping_slot.to_be_bytes();
188    compiler.compute_map_slot(&mapping_slot_bytes, &key_bytes)
189}
190
191fn get_solidity_panic_codes() -> HashMap<u64, String> {
192    let mut panic_codes = HashMap::new();
193    panic_codes.insert(0, "GenericCompilerPanic".to_string());
194    panic_codes.insert(1, "AssertionError".to_string());
195    panic_codes.insert(17, "ArithmeticOver/Underflow".to_string());
196    panic_codes.insert(18, "ZeroDivisionError".to_string());
197    panic_codes.insert(33, "UnknownEnumMember".to_string());
198    panic_codes.insert(34, "BadStorageByteArrayEncoding".to_string());
199    panic_codes.insert(51, "EmptyArray".to_string());
200    panic_codes.insert(0x32, "OutOfBounds".to_string());
201    panic_codes.insert(0x41, "OutOfMemory".to_string());
202    panic_codes.insert(0x51, "BadFunctionPointer".to_string());
203    panic_codes
204}
205
206/// Fetches the bytecode for a specified contract address, returning an error if the address is
207/// an Externally Owned Account (EOA) or if no code is associated with it.
208///
209/// This function checks the specified address on the blockchain, attempting to retrieve any
210/// contract bytecode deployed at that address. If the address corresponds to an EOA or any
211/// other address without associated bytecode, an `RpcError::EmptyResponse` error is returned.
212///
213/// # Parameters
214/// - `address`: The address of the account or contract to query, as a string.
215/// - `connection_string`: An optional RPC connection string. If not provided, the function will
216///   default to the `RPC_URL` environment variable.
217///
218/// # Returns
219/// - `Ok(Bytecode)`: The bytecode of the contract at the specified address, if present.
220/// - `Err(RpcError)`: An error if the address does not have associated bytecode, if there is an
221///   issue with the RPC connection, or if the address is invalid.
222///
223/// # Errors
224/// - Returns `RpcError::InvalidRequest` if `address` is not parsable or if no RPC URL is set.
225/// - Returns `RpcError::EmptyResponse` if the address has no associated bytecode (e.g., EOA).
226/// - Returns `RpcError::InvalidResponse` for issues with the RPC provider response.
227pub(crate) async fn get_code_for_contract(
228    address: &str,
229    connection_string: Option<String>,
230) -> Result<Bytecode, SimulationError> {
231    // Get the connection string, defaulting to the RPC_URL environment variable
232    let connection_string = connection_string.or_else(|| env::var("RPC_URL").ok());
233
234    let connection_string = match connection_string {
235        Some(url) => url,
236        None => {
237            return Err(SimulationError::FatalError(
238                "RPC_URL environment variable is not set".to_string(),
239            ))
240        }
241    };
242
243    let addr = Address::from_str(address)
244        .map_err(|_| SimulationError::FatalError(format!("Invalid address format: {address}")))?;
245    // Call eth_getCode to get the bytecode of the contract
246    match sync_get_code(&connection_string, addr) {
247        Ok(code) if code.is_empty() => {
248            Err(SimulationError::FatalError("Empty code response from RPC".to_string()))
249        }
250        Ok(code) => {
251            let bytecode = Bytecode::new_raw(Bytes::from(code.to_vec()));
252            Ok(bytecode)
253        }
254        Err(e) => match e {
255            RpcError::Transport(err) => Err(SimulationError::RecoverableError(format!(
256                "Failed to get code for contract due to internal RPC error: {err:?}"
257            ))),
258            _ => Err(SimulationError::FatalError(format!(
259                "Failed to get code for contract. Invalid response from RPC: {e:?}"
260            ))),
261        },
262    }
263}
264
265fn sync_get_code(
266    connection_string: &str,
267    addr: Address,
268) -> Result<Bytes, RpcError<TransportErrorKind>> {
269    tokio::task::block_in_place(|| {
270        tokio::runtime::Handle::current().block_on(async {
271            // Create a provider with the URL
272            let provider = ProviderBuilder::new()
273                .connect(connection_string)
274                .await?;
275            provider.get_code_at(addr).await
276        })
277    })
278}
279
280/// Converts a hexadecimal string into a fixed-size 32-byte array.
281///
282/// This function takes a string slice (e.g., a pool ID) that may or may not have
283/// a `0x` prefix. It decodes the hex string into bytes, ensuring it does not exceed
284/// 32 bytes in length. If the string is valid and fits within 32 bytes, the bytes
285/// are copied into a `[u8; 32]` array, with right zero-padding for unused bytes.
286///
287/// # Arguments
288///
289/// * `pool_id` - A string slice representing a hexadecimal pool ID. It can optionally start with
290///   the `0x` prefix.
291///
292/// # Returns
293///
294/// * `Ok([u8; 32])` - On success, returns a 32-byte array with the decoded bytes. If the input is
295///   shorter than 32 bytes, the rest of the array is right padded with zeros.
296/// * `Err(SimulationError)` - Returns an error if:
297///     - The input string is not a valid hexadecimal string.
298///     - The decoded bytes exceed 32 bytes in length.
299///
300/// # Example
301/// ```
302/// use tycho_simulation::evm::protocol::vm::utils::string_to_bytes32;
303///
304/// let pool_id = "0x1234abcd";
305/// match string_to_bytes32(pool_id) {
306///     Ok(bytes32) => println!("Bytes32: {:?}", bytes32),
307///     Err(e) => eprintln!("Error: {}", e),
308/// }
309pub fn string_to_bytes32(pool_id: &str) -> Result<[u8; 32], SimulationError> {
310    let pool_id_no_prefix =
311        if let Some(stripped) = pool_id.strip_prefix("0x") { stripped } else { pool_id };
312    let bytes = hex::decode(pool_id_no_prefix)
313        .map_err(|e| SimulationError::FatalError(format!("Invalid hex string: {e}")))?;
314    if bytes.len() > 32 {
315        return Err(SimulationError::FatalError(format!(
316            "Hex string exceeds 32 bytes: length {}",
317            bytes.len()
318        )));
319    }
320    let mut array = [0u8; 32];
321    array[..bytes.len()].copy_from_slice(&bytes);
322    Ok(array)
323}
324
325/// Decodes a JSON-encoded list of hexadecimal strings into a `Vec<Vec<u8>>`.
326///
327/// This function parses a JSON array where each element is a string representing a hexadecimal
328/// value. It converts each hex string into a vector of bytes (`Vec<u8>`), and aggregates them into
329/// a `Vec<Vec<u8>>`.
330///
331/// # Arguments
332///
333/// * `input` - A byte slice (`&[u8]`) containing JSON-encoded data. The JSON must be a valid array
334///   of hex strings.
335///
336/// # Returns
337///
338/// * `Ok(Vec<Vec<u8>>)` - On success, returns a vector of byte vectors.
339/// * `Err(SimulationError)` - Returns an error if:
340///     - The input is not valid JSON.
341///     - The JSON is not an array.
342///     - Any array element is not a string.
343///     - Any string is not a valid hexadecimal string.
344///
345/// # Example
346/// ```
347/// use tycho_simulation::evm::protocol::vm::utils::json_deserialize_address_list;
348///
349/// let json_input = br#"["0x1234", "0xc0ffee"]"#;
350/// match json_deserialize_address_list(json_input) {
351///     Ok(result) => println!("Decoded: {:?}", result),
352///     Err(e) => eprintln!("Error: {}", e),
353/// }
354/// ```
355pub fn json_deserialize_address_list(input: &[u8]) -> Result<Vec<Vec<u8>>, SimulationError> {
356    let json_value: Value = serde_json::from_slice(input)
357        .map_err(|_| SimulationError::FatalError(format!("Invalid JSON: {input:?}")))?;
358
359    if let Value::Array(hex_strings) = json_value {
360        let mut result = Vec::new();
361
362        for val in hex_strings {
363            if let Value::String(hexstring) = val {
364                let bytes = hex::decode(hexstring.trim_start_matches("0x")).map_err(|_| {
365                    SimulationError::FatalError(format!("Invalid hex string: {hexstring}"))
366                })?;
367                result.push(bytes);
368            } else {
369                return Err(SimulationError::FatalError("Array contains a non-string value".into()));
370            }
371        }
372
373        Ok(result)
374    } else {
375        Err(SimulationError::FatalError("Input is not a JSON array".into()))
376    }
377}
378
379/// Decodes a JSON-encoded list of hexadecimal strings into a `Vec<BigInt>`.
380///
381/// This function parses a JSON array where each element is a string representing a hexadecimal
382/// value. It converts each hex string into a `BigInt` using big-endian byte interpretation, and
383/// aggregates them into a `Vec<BigInt>`.
384///
385/// # Arguments
386///
387/// * `input` - A byte slice (`&[u8]`) containing JSON-encoded data. The JSON must be a valid array
388///   of hex strings.
389///
390/// # Returns
391///
392/// * `Ok(Vec<BigInt>)` - On success, returns a vector of `BigInt` values.
393/// * `Err(SimulationError)` - Returns an error if:
394///     - The input is not valid JSON.
395///     - The JSON is not an array.
396///     - Any array element is not a string.
397///     - Any string is not a valid hexadecimal string.
398///
399/// # Example
400/// ```
401/// use tycho_simulation::evm::protocol::vm::utils::json_deserialize_be_bigint_list;
402/// use num_bigint::BigInt;
403/// use tycho_simulation::evm;
404/// let json_input = br#"["0x1234", "0xdeadbeef"]"#;
405/// match json_deserialize_be_bigint_list(json_input) {
406///     Ok(result) => println!("Decoded BigInts: {:?}", result),
407///     Err(e) => eprintln!("Error: {}", e),
408/// }
409/// ```
410pub fn json_deserialize_be_bigint_list(input: &[u8]) -> Result<Vec<BigInt>, SimulationError> {
411    let json_value: Value = serde_json::from_slice(input)
412        .map_err(|_| SimulationError::FatalError(format!("Invalid JSON: {input:?}")))?;
413
414    if let Value::Array(hex_strings) = json_value {
415        let mut result = Vec::new();
416
417        for val in hex_strings {
418            if let Value::String(hexstring) = val {
419                let bytes = hex::decode(hexstring.trim_start_matches("0x")).map_err(|_| {
420                    SimulationError::FatalError(format!("Invalid hex string: {hexstring}"))
421                })?;
422                let bigint = BigInt::from_signed_bytes_be(&bytes);
423                result.push(bigint);
424            } else {
425                return Err(SimulationError::FatalError("Array contains a non-string value".into()));
426            }
427        }
428
429        Ok(result)
430    } else {
431        Err(SimulationError::FatalError("Input is not a JSON array".into()))
432    }
433}
434
435/// Load the pool's stateless/implementation contracts (the `stateless_contract_addr_{i}` state
436/// attributes) into the engine's DB so getter calls on proxy pools resolve their delegatecall
437/// targets. Curve pools are commonly EIP-1167 proxies whose implementation code is not part of the
438/// indexed pool storage; without this, getters delegatecall into empty code and revert.
439///
440/// Addresses may be static (`0x…`) or dynamic (`call:0x<factory>:method()`), and code is fetched
441/// via RPC unless provided inline as `stateless_contract_code_{i}`. The engine shares its DB with
442/// `SHARED_TYCHO_DB`, so accounts loaded here persist for later `delta_transition` rebuilds.
443pub(crate) async fn load_stateless_contracts<D: EngineDatabaseInterface + Clone + Debug>(
444    engine: &SimulationEngine<D>,
445    attributes: &HashMap<String, tycho_common::Bytes>,
446) -> Result<(), SimulationError>
447where
448    <D as DatabaseRef>::Error: Debug,
449    <D as EngineDatabaseInterface>::Error: Debug,
450{
451    let mut index = 0;
452    while let Some(encoded) = attributes.get(&format!("stateless_contract_addr_{index}")) {
453        let address = String::from_utf8(encoded.to_vec()).map_err(|e| {
454            SimulationError::FatalError(format!("stateless contract address is not UTF-8: {e}"))
455        })?;
456        let inline_code = attributes
457            .get(&format!("stateless_contract_code_{index}"))
458            .map(|value| value.to_vec());
459        index += 1;
460
461        let (account, code) = match inline_code {
462            Some(bytecode) => (address, Bytecode::new_raw(bytecode.into())),
463            None => {
464                let resolved = if address.starts_with("call") {
465                    resolve_call_address(engine, &address)?
466                } else {
467                    address
468                };
469                let code = get_code_for_contract(&resolved, None).await?;
470                (resolved, code)
471            }
472        };
473        let account: Address = account.parse().map_err(|_| {
474            SimulationError::FatalError(format!(
475                "stateless contract has an invalid address {account}"
476            ))
477        })?;
478        engine
479            .state
480            .init_account(
481                account,
482                AccountInfo {
483                    balance: U256::ZERO,
484                    nonce: 0,
485                    code_hash: code.hash_slow(),
486                    code: Some(code),
487                },
488                None,
489                false,
490            )
491            .map_err(|e| {
492                SimulationError::FatalError(format!(
493                    "stateless contract init_account failed: {e:?}"
494                ))
495            })?;
496    }
497    Ok(())
498}
499
500/// Resolve a dynamic `call:0x<address>:method()` directive to a concrete implementation address by
501/// simulating the parameterless `method()` view and decoding its returned address.
502pub(crate) fn resolve_call_address<D: EngineDatabaseInterface + Clone + Debug>(
503    engine: &SimulationEngine<D>,
504    directive: &str,
505) -> Result<String, SimulationError>
506where
507    <D as DatabaseRef>::Error: Debug,
508    <D as EngineDatabaseInterface>::Error: Debug,
509{
510    let method = directive
511        .split(':')
512        .next_back()
513        .ok_or_else(|| {
514            SimulationError::FatalError(format!("malformed stateless call directive {directive}"))
515        })?;
516    let to: Address = directive
517        .split(':')
518        .nth(1)
519        .ok_or_else(|| {
520            SimulationError::FatalError(format!(
521                "stateless call directive is missing its target {directive}"
522            ))
523        })?
524        .parse()
525        .map_err(|_| {
526            SimulationError::FatalError(format!(
527                "stateless call directive has an invalid target {directive}"
528            ))
529        })?;
530    let mut hasher = Keccak256::new();
531    hasher.update(method.as_bytes());
532    let selector = hasher.finalize()[..4].to_vec();
533    let res = engine
534        .simulate(&SimulationParameters {
535            caller: Address::ZERO,
536            to,
537            data: selector,
538            ..Default::default()
539        })
540        .map_err(|e| SimulationError::FatalError(format!("stateless call failed: {e}")))?;
541    let address = Address::abi_decode(res.result.as_ref())
542        .map_err(|e| SimulationError::FatalError(format!("stateless call decode failed: {e}")))?;
543    Ok(address.to_string())
544}
545
546#[cfg(test)]
547mod tests {
548    use dotenv::dotenv;
549
550    use super::*;
551    use crate::utils::hexstring_to_vec;
552
553    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
554    #[cfg_attr(not(feature = "network_tests"), ignore)]
555    async fn test_get_code_for_address() {
556        let rpc_url = env::var("RPC_URL").unwrap_or_else(|_| {
557            dotenv().expect("Missing .env file");
558            env::var("RPC_URL").expect("Missing RPC_URL in .env file")
559        });
560
561        let address = "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640";
562        let result = get_code_for_contract(address, Some(rpc_url)).await;
563
564        assert!(result.is_ok(), "Network call should not fail");
565
566        let code = result.unwrap();
567        assert!(!code.bytes().is_empty(), "Code should not be empty");
568    }
569
570    #[test]
571    fn test_maybe_coerce_error_revert_no_gas_info() {
572        let err = SimulationEngineError::TransactionError{
573            data: "0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000011496e76616c6964206f7065726174696f6e000000000000000000000000000000".to_string(),
574            gas_used: None
575        };
576
577        let result = coerce_error(&err, "test_pool", None);
578
579        if let SimulationError::FatalError(msg) = result {
580            assert!(msg.contains("Simulation reverted for unknown reason: Invalid operation"));
581        } else {
582            panic!("Expected SolidityError error");
583        }
584    }
585
586    #[test]
587    fn test_maybe_coerce_error_out_of_gas() {
588        // Test out-of-gas situation with gas limit and gas used provided
589        let err = SimulationEngineError::TransactionError{
590            data: "0x08c379a000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000011496e76616c6964206f7065726174696f6e000000000000000000000000000000".to_string(),
591            gas_used: Some(980)
592        };
593
594        let result = coerce_error(&err, "test_pool", Some(1000));
595
596        if let SimulationError::InvalidInput(message, _partial_result) = result {
597            assert!(message.contains("Used: 98.00% of gas limit."));
598            assert!(message.contains("test_pool"));
599        } else {
600            panic!("Expected OutOfGas error");
601        }
602    }
603
604    #[test]
605    fn test_maybe_coerce_error_no_gas_limit_info() {
606        // Test out-of-gas situation without gas limit info
607        let err = SimulationEngineError::TransactionError {
608            data: "OutOfGas".to_string(),
609            gas_used: None,
610        };
611
612        let result = coerce_error(&err, "test_pool", None);
613
614        if let SimulationError::InvalidInput(message, _partial_result) = result {
615            assert!(message.contains("Original error: OutOfGas"));
616            assert!(message.contains("Pool state: test_pool"));
617        } else {
618            panic!("Expected RetryDifferentInput error");
619        }
620    }
621
622    #[test]
623    fn test_maybe_coerce_error_storage_error() {
624        let err = SimulationEngineError::StorageError("Storage error:".to_string());
625
626        let result = coerce_error(&err, "test_pool", None);
627
628        if let SimulationError::RecoverableError(message) = result {
629            assert_eq!(message, "Storage error:");
630        } else {
631            println!("{result:?}");
632            panic!("Expected RetryLater error");
633        }
634    }
635
636    #[test]
637    fn test_maybe_coerce_error_no_match() {
638        // Test for non-revert, non-out-of-gas, non-storage errors
639        let err = SimulationEngineError::TransactionError {
640            data: "Some other error".to_string(),
641            gas_used: None,
642        };
643
644        let result = coerce_error(&err, "test_pool", None);
645
646        if let SimulationError::FatalError(message) = result {
647            assert_eq!(message, "TransactionError: Some other error");
648        } else {
649            panic!("Expected solidity error");
650        }
651    }
652
653    #[test]
654    fn test_parse_solidity_error_message_error_string() {
655        // Test parsing Solidity Error(string) message
656        let data = "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e416d6f756e7420746f6f206c6f77000000000000000000000000000000000000";
657
658        let result = parse_solidity_error_message(data);
659
660        assert_eq!(result, "Amount too low");
661    }
662
663    #[test]
664    fn test_parse_solidity_error_message_panic_code() {
665        // Test parsing Solidity Panic(uint256) message
666        let data = "0x4e487b710000000000000000000000000000000000000000000000000000000000000001";
667
668        let result = parse_solidity_error_message(data);
669
670        assert_eq!(result, "AssertionError");
671    }
672
673    #[test]
674    fn test_parse_solidity_error_message_failed_to_decode() {
675        // Test failed decoding with invalid data
676        let data = "0x1234567890";
677
678        let result = parse_solidity_error_message(data);
679
680        assert!(result.contains("Failed to decode"));
681    }
682
683    #[test]
684    fn test_hexstring_to_vec() {
685        let hexstring = "0x68656c6c6f";
686        let expected = vec![0x68, 0x65, 0x6c, 0x6c, 0x6f];
687        let result = hexstring_to_vec(hexstring).unwrap();
688        assert_eq!(result, expected);
689    }
690
691    #[test]
692    fn test_hexstring_to_vec_no_prefix() {
693        let hexstring = "68656c6c6f";
694        let expected = vec![0x68, 0x65, 0x6c, 0x6c, 0x6f];
695        let result = hexstring_to_vec(hexstring).unwrap();
696        assert_eq!(result, expected);
697    }
698
699    #[test]
700    fn test_hexstring_to_vec_invalid_characters() {
701        let hexstring = "0x68656c6c6z"; // Invalid character 'z'
702        let result = hexstring_to_vec(hexstring);
703        assert!(result.is_err());
704        if let Err(SimulationError::FatalError(msg)) = result {
705            assert!(msg.contains("Invalid hex string"));
706        } else {
707            panic!("Expected EncodingError");
708        }
709    }
710
711    #[test]
712    fn test_json_deserialize_address_list() {
713        let json_input = r#"["0x1234","0xabcd"]"#.as_bytes();
714        let result = json_deserialize_address_list(json_input).unwrap();
715        assert_eq!(result, vec![vec![0x12, 0x34], vec![0xab, 0xcd]]);
716    }
717
718    #[test]
719    fn test_json_deserialize_bigint_list() {
720        let json_input = r#"["0x0b1a2bc2ec500000","0x02c68af0bb140000"]"#.as_bytes();
721        let result = json_deserialize_be_bigint_list(json_input).unwrap();
722        assert_eq!(
723            result,
724            vec![BigInt::from(800000000000000000u64), BigInt::from(200000000000000000u64)]
725        );
726    }
727
728    #[test]
729    fn test_invalid_deserialize_address_list() {
730        let json_input = r#"["invalid_hex"]"#.as_bytes();
731        let result = json_deserialize_address_list(json_input);
732        assert!(result.is_err());
733    }
734
735    #[test]
736    fn test_invalid_deserialize_bigint_list() {
737        let json_input = r#"["invalid_hex"]"#.as_bytes();
738        let result = json_deserialize_be_bigint_list(json_input);
739        assert!(result.is_err());
740    }
741}