use std::{
collections::{HashMap, HashSet},
fs,
path::Path,
};
use tracing::info;
use tycho_client::{
rpc::{HttpRPCClientOptions, RPCClient, RPC_CLIENT_CONCURRENCY},
HttpRPCClient, RPCError,
};
use tycho_common::{
models::{token::Token, Chain},
simulation::errors::SimulationError,
Bytes,
};
pub fn hexstring_to_vec(hexstring: &str) -> Result<Vec<u8>, SimulationError> {
let hexstring_no_prefix =
if let Some(stripped) = hexstring.strip_prefix("0x") { stripped } else { hexstring };
let bytes = hex::decode(hexstring_no_prefix).map_err(|err| {
SimulationError::FatalError(format!("Invalid hex string `{hexstring}`: {err}"))
})?;
Ok(bytes)
}
pub async fn load_all_tokens(
tycho_url: &str,
no_tls: bool,
auth_key: Option<&str>,
compression: bool,
chain: Chain,
min_quality: Option<i32>,
max_days_since_last_trade: Option<u64>,
) -> Result<HashMap<Bytes, Token>, SimulationError> {
info!("Loading tokens from Tycho...");
let rpc_url =
if no_tls { format!("http://{tycho_url}") } else { format!("https://{tycho_url}") };
let rpc_options = HttpRPCClientOptions::new()
.with_auth_key(auth_key.map(|s| s.to_string()))
.with_compression(compression);
let rpc_client = HttpRPCClient::new(rpc_url.as_str(), rpc_options)
.map_err(|err| map_rpc_error(err, "Failed to create Tycho RPC client"))?;
let default_min_days = HashMap::from([(Chain::Base, 1_u64), (Chain::Unichain, 14_u64)]);
#[allow(clippy::mutable_key_type)]
let tokens = rpc_client
.get_all_tokens(
chain.into(),
min_quality.or(Some(100)),
max_days_since_last_trade.or(default_min_days
.get(&chain)
.or(Some(&42))
.copied()),
None,
RPC_CLIENT_CONCURRENCY,
)
.await
.map_err(|err| map_rpc_error(err, "Unable to load tokens"))?;
tokens
.into_iter()
.map(|token| {
let token_clone = token.clone();
Token::try_from(token)
.map(|converted| (converted.address.clone(), converted))
.map_err(|_| {
SimulationError::FatalError(format!(
"Unable to convert token `{symbol}` at {address} on chain {chain} into ERC20 token",
symbol = token_clone.symbol,
address = token_clone.address,
chain = token_clone.chain,
))
})
})
.collect()
}
pub fn get_default_url(chain: &Chain) -> Option<String> {
match chain {
Chain::Ethereum => Some("tycho-beta.propellerheads.xyz".to_string()),
Chain::Base => Some("tycho-base-beta.propellerheads.xyz".to_string()),
Chain::Unichain => Some("tycho-unichain-beta.propellerheads.xyz".to_string()),
_ => None,
}
}
pub fn load_blocklist(path: Option<&Path>) -> Result<HashSet<String>, SimulationError> {
let Some(path) = path else {
return Ok(default_blocklist());
};
let contents = fs::read_to_string(path).map_err(|e| {
SimulationError::FatalError(format!("Failed to read blocklist {:?}: {e}", path))
})?;
parse_blocklist(&contents).map_err(|e| {
SimulationError::FatalError(format!("Failed to parse blocklist {:?}: {e}", path))
})
}
pub fn default_blocklist() -> HashSet<String> {
parse_blocklist(include_str!("../blocklist.toml"))
.expect("embedded blocklist.toml is valid TOML")
}
fn parse_blocklist(contents: &str) -> Result<HashSet<String>, toml::de::Error> {
#[derive(Default, serde::Deserialize)]
struct Blocklist {
#[serde(default)]
components: HashSet<String>,
}
#[derive(serde::Deserialize)]
struct BlocklistConfig {
#[serde(default)]
blocklist: Blocklist,
}
let config: BlocklistConfig = toml::from_str(contents)?;
Ok(config.blocklist.components)
}
fn map_rpc_error(err: RPCError, context: &str) -> SimulationError {
let message = format!("{context}: {err}", err = err,);
match err {
RPCError::UrlParsing(_, _) | RPCError::FormatRequest(_) => {
SimulationError::InvalidInput(message, None)
}
_ => SimulationError::FatalError(message),
}
}