use std::{
collections::{HashMap, HashSet},
fs,
path::Path,
};
use anyhow::{Context, Result};
pub use fynd_core::PoolConfig;
use serde::{Deserialize, Serialize};
const DEFAULT_WORKER_POOLS_TOML: &str = r#"
[pools.bellman_ford_2_hops]
algorithm = "bellman_ford"
num_workers = 3
task_queue_capacity = 1000
max_hops = 2
timeout_ms = 500
"#;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerPoolsConfig {
pools: HashMap<String, PoolConfig>,
}
impl WorkerPoolsConfig {
pub fn new(pools: HashMap<String, PoolConfig>) -> Self {
Self { pools }
}
pub fn pools(&self) -> &HashMap<String, PoolConfig> {
&self.pools
}
pub fn into_pools(self) -> HashMap<String, PoolConfig> {
self.pools
}
pub fn builtin_default() -> Self {
toml::from_str(DEFAULT_WORKER_POOLS_TOML).expect("built-in worker_pools.toml is valid TOML")
}
pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let contents = fs::read_to_string(path)
.with_context(|| format!("failed to read config file {}", path.display()))?;
toml::from_str(&contents)
.with_context(|| format!("failed to parse config file {}", path.display()))
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BlocklistConfig {
#[serde(default)]
components: HashSet<String>,
}
impl BlocklistConfig {
pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let contents = fs::read_to_string(path)
.with_context(|| format!("failed to read blocklist config {}", path.display()))?;
#[derive(Deserialize)]
struct Wrapper {
blocklist: BlocklistConfig,
}
let wrapper: Wrapper = toml::from_str(&contents)
.with_context(|| format!("failed to parse blocklist config {}", path.display()))?;
Ok(wrapper.blocklist)
}
pub fn into_components(self) -> HashSet<String> {
self.components
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builtin_default_does_not_panic() {
let config = WorkerPoolsConfig::builtin_default();
assert!(!config.pools().is_empty(), "built-in config must have at least one pool");
}
#[test]
fn test_pool_config_minimal_uses_defaults() {
let toml = r#"
[pools.basic]
algorithm = "most_liquid"
"#;
let config: WorkerPoolsConfig = toml::from_str(toml).unwrap();
let pool = &config.pools()["basic"];
assert_eq!(pool.algorithm(), "most_liquid");
assert_eq!(pool.num_workers(), num_cpus::get());
use fynd_core::solver::defaults as core_defaults;
assert_eq!(pool.task_queue_capacity(), core_defaults::POOL_TASK_QUEUE_CAPACITY);
assert_eq!(pool.min_hops(), core_defaults::POOL_MIN_HOPS);
assert_eq!(pool.max_hops(), core_defaults::POOL_MAX_HOPS);
assert_eq!(pool.timeout_ms(), core_defaults::POOL_TIMEOUT_MS);
assert_eq!(pool.max_routes(), None);
}
#[test]
fn test_pool_config_all_fields_explicit() {
let toml = r#"
[pools.custom]
algorithm = "most_liquid"
num_workers = 8
task_queue_capacity = 500
min_hops = 2
max_hops = 4
timeout_ms = 200
max_routes = 50
"#;
let config: WorkerPoolsConfig = toml::from_str(toml).unwrap();
let pool = &config.pools()["custom"];
assert_eq!(pool.algorithm(), "most_liquid");
assert_eq!(pool.num_workers(), 8);
assert_eq!(pool.task_queue_capacity(), 500);
assert_eq!(pool.min_hops(), 2);
assert_eq!(pool.max_hops(), 4);
assert_eq!(pool.timeout_ms(), 200);
assert_eq!(pool.max_routes(), Some(50));
}
}
pub mod defaults {
pub use fynd_core::solver::defaults::{
GAS_REFRESH_INTERVAL, MIN_TOKEN_QUALITY, RECONNECT_DELAY, ROUTER_MIN_RESPONSES,
TRADED_N_DAYS_AGO, TVL_BUFFER_RATIO,
};
pub const HTTP_HOST: &str = "0.0.0.0";
pub const HTTP_PORT: u16 = 3000;
pub const DEFAULT_RPC_URL: &str = "https://eth.llamarpc.com";
pub const MIN_TVL: f64 = 10.0;
pub const WORKER_ROUTER_TIMEOUT_MS: u64 = 100;
pub fn default_tycho_url(chain: &str) -> Result<&str, String> {
match chain.to_lowercase().as_str() {
"ethereum" => Ok("tycho-fynd-ethereum.propellerheads.xyz"),
"base" => Ok("tycho-fynd-base.propellerheads.xyz"),
"unichain" => Ok("tycho-fynd-unichain.propellerheads.xyz"),
other => Err(format!(
"no default Tycho URL for chain '{}'. Please provide --tycho-url explicitly.",
other
)),
}
}
}