use std::fmt;
use alloy_primitives::Address;
use foldhash::HashMap;
use super::chain::{Env, SupportedChainId};
type TokenEntry = (Address, u8);
#[derive(Debug)]
pub struct TokenRegistry {
inner: HashMap<String, TokenEntry>,
}
impl TokenRegistry {
#[must_use]
pub fn new(entries: impl IntoIterator<Item = (impl Into<String>, Address)>) -> Self {
Self { inner: entries.into_iter().map(|(k, v)| (k.into(), (v, 18))).collect() }
}
#[must_use]
pub fn new_with_decimals(
entries: impl IntoIterator<Item = (impl Into<String>, Address, u8)>,
) -> Self {
Self { inner: entries.into_iter().map(|(k, v, d)| (k.into(), (v, d))).collect() }
}
#[must_use]
pub fn get(&self, asset: &str) -> Option<Address> {
self.inner.get(asset).map(|&(addr, _)| addr)
}
#[must_use]
pub fn get_decimals(&self, asset: &str) -> Option<u8> {
self.inner.get(asset).map(|&(_, decimals)| decimals)
}
pub fn insert(&mut self, symbol: impl Into<String>, address: Address) {
self.inner.insert(symbol.into(), (address, 18));
}
pub fn insert_with_decimals(
&mut self,
symbol: impl Into<String>,
address: Address,
decimals: u8,
) {
self.inner.insert(symbol.into(), (address, decimals));
}
#[must_use]
pub fn contains(&self, asset: &str) -> bool {
self.inner.contains_key(asset)
}
#[must_use]
pub fn len(&self) -> usize {
self.inner.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[must_use]
pub fn get_entry(&self, asset: &str) -> Option<(Address, u8)> {
self.inner.get(asset).copied()
}
pub fn remove(&mut self, asset: &str) -> Option<(Address, u8)> {
self.inner.remove(asset)
}
}
impl fmt::Display for TokenRegistry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "registry({} tokens)", self.inner.len())
}
}
#[derive(Debug)]
pub struct CowSwapConfig {
pub chain_id: SupportedChainId,
pub env: Env,
pub sell_token: Address,
pub sell_token_decimals: u8,
pub tokens: TokenRegistry,
pub slippage_bps: u32,
pub order_valid_secs: u32,
pub receiver: Option<Address>,
}
impl CowSwapConfig {
#[must_use]
pub const fn prod(
chain_id: SupportedChainId,
sell_token: Address,
tokens: TokenRegistry,
slippage_bps: u32,
order_valid_secs: u32,
) -> Self {
Self {
chain_id,
env: Env::Prod,
sell_token,
sell_token_decimals: 18,
tokens,
slippage_bps,
order_valid_secs,
receiver: None,
}
}
#[must_use]
pub const fn staging(
chain_id: SupportedChainId,
sell_token: Address,
tokens: TokenRegistry,
slippage_bps: u32,
order_valid_secs: u32,
) -> Self {
Self {
chain_id,
env: Env::Staging,
sell_token,
sell_token_decimals: 18,
tokens,
slippage_bps,
order_valid_secs,
receiver: None,
}
}
#[must_use]
pub const fn with_sell_token(mut self, token: Address) -> Self {
self.sell_token = token;
self
}
#[must_use]
pub const fn with_chain_id(mut self, chain_id: SupportedChainId) -> Self {
self.chain_id = chain_id;
self
}
#[must_use]
pub const fn with_env(mut self, env: Env) -> Self {
self.env = env;
self
}
#[must_use]
pub const fn with_slippage_bps(mut self, slippage_bps: u32) -> Self {
self.slippage_bps = slippage_bps;
self
}
#[must_use]
pub const fn with_order_valid_secs(mut self, secs: u32) -> Self {
self.order_valid_secs = secs;
self
}
#[must_use]
pub const fn with_sell_token_decimals(mut self, decimals: u8) -> Self {
self.sell_token_decimals = decimals;
self
}
#[must_use]
pub const fn with_receiver(mut self, receiver: Address) -> Self {
self.receiver = Some(receiver);
self
}
#[must_use]
pub const fn has_custom_receiver(&self) -> bool {
self.receiver.is_some()
}
#[must_use]
pub fn effective_receiver(&self, default: Address) -> Address {
self.receiver.map_or(default, |r| r)
}
}
impl fmt::Display for CowSwapConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "config({}, {}, sell={:#x})", self.chain_id, self.env, self.sell_token)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_registry_new_defaults_to_18_decimals() {
let reg = TokenRegistry::new([("WETH", Address::ZERO)]);
assert_eq!(reg.get_decimals("WETH"), Some(18));
}
#[test]
fn token_registry_new_with_decimals() {
let reg = TokenRegistry::new_with_decimals([("USDC", Address::ZERO, 6u8)]);
assert_eq!(reg.get_decimals("USDC"), Some(6));
assert_eq!(reg.get("USDC"), Some(Address::ZERO));
}
#[test]
fn token_registry_insert_and_contains() {
let mut reg = TokenRegistry::new(std::iter::empty::<(&str, Address)>());
assert!(reg.is_empty());
assert_eq!(reg.len(), 0);
assert!(!reg.contains("DAI"));
reg.insert("DAI", Address::ZERO);
assert!(reg.contains("DAI"));
assert_eq!(reg.len(), 1);
assert!(!reg.is_empty());
}
#[test]
fn token_registry_insert_with_decimals() {
let mut reg = TokenRegistry::new(std::iter::empty::<(&str, Address)>());
reg.insert_with_decimals("WBTC", Address::ZERO, 8);
assert_eq!(reg.get_decimals("WBTC"), Some(8));
}
#[test]
fn token_registry_get_entry() {
let reg = TokenRegistry::new_with_decimals([("USDC", Address::ZERO, 6u8)]);
assert_eq!(reg.get_entry("USDC"), Some((Address::ZERO, 6)));
assert_eq!(reg.get_entry("NONEXISTENT"), None);
}
#[test]
fn token_registry_remove() {
let mut reg = TokenRegistry::new([("WETH", Address::ZERO)]);
assert!(reg.contains("WETH"));
let removed = reg.remove("WETH");
assert!(removed.is_some());
assert!(!reg.contains("WETH"));
assert!(reg.is_empty());
}
#[test]
fn token_registry_remove_nonexistent() {
let mut reg = TokenRegistry::new(std::iter::empty::<(&str, Address)>());
assert!(reg.remove("WETH").is_none());
}
#[test]
fn token_registry_get_missing_returns_none() {
let reg = TokenRegistry::new(std::iter::empty::<(&str, Address)>());
assert_eq!(reg.get("WETH"), None);
assert_eq!(reg.get_decimals("WETH"), None);
}
#[test]
fn token_registry_display() {
let reg = TokenRegistry::new([("A", Address::ZERO), ("B", Address::ZERO)]);
let s = format!("{reg}");
assert!(s.contains("2 tokens"));
}
fn empty_registry() -> TokenRegistry {
TokenRegistry::new(std::iter::empty::<(&str, Address)>())
}
#[test]
fn config_prod_defaults() {
let cfg = CowSwapConfig::prod(
SupportedChainId::Mainnet,
Address::ZERO,
empty_registry(),
50,
1800,
);
assert!(cfg.env.is_prod());
assert_eq!(cfg.slippage_bps, 50);
assert_eq!(cfg.order_valid_secs, 1800);
assert_eq!(cfg.sell_token_decimals, 18);
assert!(!cfg.has_custom_receiver());
}
#[test]
fn config_staging_defaults() {
let cfg = CowSwapConfig::staging(
SupportedChainId::Sepolia,
Address::ZERO,
empty_registry(),
100,
900,
);
assert!(cfg.env.is_staging());
assert_eq!(cfg.slippage_bps, 100);
}
#[test]
fn config_builder_methods() {
let cfg = CowSwapConfig::prod(
SupportedChainId::Mainnet,
Address::ZERO,
empty_registry(),
50,
1800,
)
.with_slippage_bps(100)
.with_order_valid_secs(600)
.with_sell_token_decimals(6)
.with_chain_id(SupportedChainId::Sepolia)
.with_env(Env::Staging);
assert_eq!(cfg.slippage_bps, 100);
assert_eq!(cfg.order_valid_secs, 600);
assert_eq!(cfg.sell_token_decimals, 6);
assert_eq!(cfg.chain_id, SupportedChainId::Sepolia);
assert!(cfg.env.is_staging());
}
#[test]
fn config_with_receiver() {
let recv = Address::new([0x01; 20]);
let wallet = Address::new([0x02; 20]);
let cfg = CowSwapConfig::prod(
SupportedChainId::Mainnet,
Address::ZERO,
empty_registry(),
50,
1800,
)
.with_receiver(recv);
assert!(cfg.has_custom_receiver());
assert_eq!(cfg.effective_receiver(wallet), recv);
}
#[test]
fn config_effective_receiver_defaults_to_wallet() {
let wallet = Address::new([0x02; 20]);
let cfg = CowSwapConfig::prod(
SupportedChainId::Mainnet,
Address::ZERO,
empty_registry(),
50,
1800,
);
assert_eq!(cfg.effective_receiver(wallet), wallet);
}
#[test]
fn config_with_sell_token() {
let token = Address::new([0xaa; 20]);
let cfg = CowSwapConfig::prod(
SupportedChainId::Mainnet,
Address::ZERO,
empty_registry(),
50,
1800,
)
.with_sell_token(token);
assert_eq!(cfg.sell_token, token);
}
#[test]
fn config_display() {
let cfg = CowSwapConfig::prod(
SupportedChainId::Mainnet,
Address::ZERO,
empty_registry(),
50,
1800,
);
let s = format!("{cfg}");
assert!(s.contains("config("));
}
}