Skip to main content

tycho_simulation/
utils.rs

1use std::{
2    collections::{HashMap, HashSet},
3    fs,
4    path::Path,
5};
6
7use tracing::info;
8use tycho_client::{
9    rpc::{AllTokensParams, 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 min_q = min_quality.or(Some(100));
90    let traded = max_days_since_last_trade.or(default_min_days
91        .get(&chain)
92        .or(Some(&42))
93        .copied());
94    let mut token_params = AllTokensParams::new(chain, RPC_CLIENT_CONCURRENCY);
95    if let Some(q) = min_q {
96        token_params = token_params.with_min_quality(q);
97    }
98    if let Some(d) = traded {
99        token_params = token_params.with_traded_n_days_ago(d);
100    }
101    let tokens = rpc_client
102        .get_all_tokens(token_params)
103        .await
104        .map_err(|err| map_rpc_error(err, "Unable to load tokens"))?;
105
106    tokens
107        .into_iter()
108        .map(|token| Ok((token.address.clone(), token)))
109        .collect()
110}
111
112/// Get the default Tycho URL for the given chain.
113pub fn get_default_url(chain: &Chain) -> Option<String> {
114    match chain {
115        Chain::Ethereum => Some("tycho-beta.propellerheads.xyz".to_string()),
116        Chain::Base => Some("tycho-base-beta.propellerheads.xyz".to_string()),
117        Chain::Unichain => Some("tycho-unichain-beta.propellerheads.xyz".to_string()),
118        Chain::Bsc => Some("tycho-bsc-beta.propellerheads.xyz".to_string()),
119        Chain::Arbitrum => Some("tycho-arbitrum-beta.propellerheads.xyz".to_string()),
120        _ => None,
121    }
122}
123
124/// Loads blocklisted component IDs from a TOML file.
125///
126/// If `path` is `Some`, reads from that file and returns an error if
127/// the file is missing or invalid. If `None`, returns the default
128/// blocklist embedded in the library.
129pub fn load_blocklist(path: Option<&Path>) -> Result<HashSet<String>, SimulationError> {
130    let Some(path) = path else {
131        return Ok(default_blocklist());
132    };
133    let contents = fs::read_to_string(path).map_err(|e| {
134        SimulationError::FatalError(format!("Failed to read blocklist {:?}: {e}", path))
135    })?;
136    parse_blocklist(&contents).map_err(|e| {
137        SimulationError::FatalError(format!("Failed to parse blocklist {:?}: {e}", path))
138    })
139}
140
141pub fn default_blocklist() -> HashSet<String> {
142    parse_blocklist(include_str!("../blocklist.toml"))
143        .expect("embedded blocklist.toml is valid TOML")
144}
145
146fn parse_blocklist(contents: &str) -> Result<HashSet<String>, toml::de::Error> {
147    #[derive(Default, serde::Deserialize)]
148    struct Blocklist {
149        #[serde(default)]
150        components: HashSet<String>,
151    }
152
153    #[derive(serde::Deserialize)]
154    struct BlocklistConfig {
155        #[serde(default)]
156        blocklist: Blocklist,
157    }
158
159    let config: BlocklistConfig = toml::from_str(contents)?;
160    Ok(config.blocklist.components)
161}
162
163fn map_rpc_error(err: RPCError, context: &str) -> SimulationError {
164    let message = format!("{context}: {err}", err = err,);
165    match err {
166        RPCError::UrlParsing(_, _) | RPCError::FormatRequest(_) => {
167            SimulationError::InvalidInput(message, None)
168        }
169        _ => SimulationError::FatalError(message),
170    }
171}