Skip to main content

tycho_simulation/
utils.rs

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