tycho_simulation/
utils.rs

1use std::collections::HashMap;
2
3use tracing::info;
4use tycho_client::{
5    rpc::{HttpRPCClientOptions, RPCClient, RPC_CLIENT_CONCURRENCY},
6    HttpRPCClient, RPCError,
7};
8use tycho_common::{
9    models::{token::Token, Chain},
10    simulation::errors::SimulationError,
11    Bytes,
12};
13
14/// Converts a hexadecimal string into a `Vec<u8>`.
15///
16/// This function accepts a hexadecimal string with or without the `0x` prefix. If the prefix
17/// is present, it is removed before decoding. The remaining string is expected to be a valid
18/// hexadecimal representation, otherwise an error is returned.
19///
20/// # Arguments
21///
22/// * `hexstring` - A string slice containing the hexadecimal string. It may optionally start with
23///   `0x`.
24///
25/// # Returns
26///
27/// * `Ok(Vec<u8>)` - A vector of bytes decoded from the hexadecimal string.
28/// * `Err(SimulationError)` - An error if the input string is not a valid hexadecimal
29///   representation.
30///
31/// # Errors
32///
33/// This function returns a `SimulationError::FatalError` if:
34/// - The string contains invalid hexadecimal characters.
35/// - The string is empty or malformed.
36pub fn hexstring_to_vec(hexstring: &str) -> Result<Vec<u8>, SimulationError> {
37    let hexstring_no_prefix =
38        if let Some(stripped) = hexstring.strip_prefix("0x") { stripped } else { hexstring };
39    let bytes = hex::decode(hexstring_no_prefix).map_err(|err| {
40        SimulationError::FatalError(format!("Invalid hex string `{hexstring}`: {err}"))
41    })?;
42    Ok(bytes)
43}
44
45/// Loads all tokens from Tycho and returns them as a Hashmap of address->Token.
46///
47/// # Arguments
48///
49/// * `tycho_url` - The URL of the Tycho RPC (do not include the url prefix e.g. 'https://').
50/// * `no_tls` - Whether to use HTTP instead of HTTPS.
51/// * `auth_key` - The API key to use for authentication.
52/// * `chain` - The chain to load tokens from.
53/// * `min_quality` - The minimum quality of tokens to load. Defaults to 100 if not provided.
54/// * `max_days_since_last_trade` - The max number of days since the token was last traded. Defaults
55///   are chain specific and applied if not provided.
56///
57/// # Returns
58///
59/// * `Ok(HashMap<Bytes, Token>)` - A mapping from token address to token metadata loaded from Tycho
60/// * `Err(SimulationError)` - An error indicating why the token list could not be loaded.
61pub async fn load_all_tokens(
62    tycho_url: &str,
63    no_tls: bool,
64    auth_key: Option<&str>,
65    compression: bool,
66    chain: Chain,
67    min_quality: Option<i32>,
68    max_days_since_last_trade: Option<u64>,
69) -> Result<HashMap<Bytes, Token>, SimulationError> {
70    info!("Loading tokens from Tycho...");
71    let rpc_url =
72        if no_tls { format!("http://{tycho_url}") } else { format!("https://{tycho_url}") };
73
74    let rpc_options = HttpRPCClientOptions::new()
75        .with_auth_key(auth_key.map(|s| s.to_string()))
76        .with_compression(compression);
77
78    let rpc_client = HttpRPCClient::new(rpc_url.as_str(), rpc_options)
79        .map_err(|err| map_rpc_error(err, "Failed to create Tycho RPC client"))?;
80
81    // Chain specific defaults for special case chains. Otherwise defaults to 42 days.
82    let default_min_days = HashMap::from([(Chain::Base, 1_u64), (Chain::Unichain, 14_u64)]);
83
84    #[allow(clippy::mutable_key_type)]
85    let tokens = rpc_client
86        .get_all_tokens(
87            chain.into(),
88            min_quality.or(Some(100)),
89            max_days_since_last_trade.or(default_min_days
90                .get(&chain)
91                .or(Some(&42))
92                .copied()),
93            Some(3000),
94            RPC_CLIENT_CONCURRENCY,
95        )
96        .await
97        .map_err(|err| map_rpc_error(err, "Unable to load tokens"))?;
98
99    tokens
100        .into_iter()
101        .map(|token| {
102            let token_clone = token.clone();
103            Token::try_from(token)
104                .map(|converted| (converted.address.clone(), converted))
105                .map_err(|_| {
106                    SimulationError::FatalError(format!(
107                        "Unable to convert token `{symbol}` at {address} on chain {chain} into ERC20 token",
108                        symbol = token_clone.symbol,
109                        address = token_clone.address,
110                        chain = token_clone.chain,
111                    ))
112                })
113        })
114        .collect()
115}
116
117/// Get the default Tycho URL for the given chain.
118pub fn get_default_url(chain: &Chain) -> Option<String> {
119    match chain {
120        Chain::Ethereum => Some("tycho-beta.propellerheads.xyz".to_string()),
121        Chain::Base => Some("tycho-base-beta.propellerheads.xyz".to_string()),
122        Chain::Unichain => Some("tycho-unichain-beta.propellerheads.xyz".to_string()),
123        _ => None,
124    }
125}
126
127fn map_rpc_error(err: RPCError, context: &str) -> SimulationError {
128    let message = format!("{context}: {err}", err = err,);
129    match err {
130        RPCError::UrlParsing(_, _) | RPCError::FormatRequest(_) => {
131            SimulationError::InvalidInput(message, None)
132        }
133        _ => SimulationError::FatalError(message),
134    }
135}