use alloy_primitives::{Address, U256};
use cow_chains::SupportedChainId;
use cow_types::CowHook;
use foldhash::{HashMap, HashSet};
use crate::{
sdk::HOOK_DAPP_BRIDGE_PROVIDER_PREFIX,
types::{BridgeError, MultiQuoteResult},
};
const PCT_100_PERCENT: u128 = 10u128.pow(18);
#[must_use]
pub fn apply_bps(amount: U256, bps: u32) -> U256 {
(amount * U256::from(10_000u32 - bps)) / U256::from(10_000u32)
}
pub fn apply_pct_fee(amount: U256, pct: u128) -> Result<U256, BridgeError> {
if pct > PCT_100_PERCENT {
return Err(BridgeError::QuoteError("fee cannot exceed 100%".to_owned()));
}
let factor = U256::from(PCT_100_PERCENT - pct);
Ok((amount * factor) / U256::from(PCT_100_PERCENT))
}
pub fn pct_to_bps(pct: u128) -> Result<u32, BridgeError> {
if pct > PCT_100_PERCENT {
return Err(BridgeError::QuoteError("fee cannot exceed 100%".to_owned()));
}
let bps = (pct * 10_000) / PCT_100_PERCENT;
Ok(bps as u32)
}
pub fn calculate_fee_bps(fee_amount: U256, amount: U256) -> Result<u32, BridgeError> {
if amount.is_zero() {
return Err(BridgeError::QuoteError("denominator is zero".to_owned()));
}
if fee_amount > amount {
return Err(BridgeError::QuoteError("fee amount is greater than amount".to_owned()));
}
let numerator = fee_amount * U256::from(10_000u32) + amount / U256::from(2u32);
let bps = numerator / amount;
Ok(bps.to::<u64>() as u32)
}
#[must_use]
pub const fn calculate_deadline(now_secs: u64, duration_secs: u64) -> u64 {
now_secs.saturating_add(duration_secs)
}
#[must_use]
pub fn are_hooks_equal(hook_a: &CowHook, hook_b: &CowHook) -> bool {
hook_a.call_data == hook_b.call_data &&
hook_a.gas_limit == hook_b.gas_limit &&
hook_a.target == hook_b.target
}
#[must_use]
pub fn hook_mock_for_cost_estimation(gas_limit: u64) -> CowHook {
CowHook {
call_data: "0x00".to_owned(),
gas_limit: gas_limit.to_string(),
target: "0x0000000000000000000000000000000000000000".to_owned(),
dapp_id: Some(HOOK_DAPP_BRIDGE_PROVIDER_PREFIX.to_owned()),
}
}
#[must_use]
pub fn get_post_hooks(full_app_data: &str) -> Vec<CowHook> {
let Ok(value) = serde_json::from_str::<serde_json::Value>(full_app_data) else {
return vec![];
};
value
.get("metadata")
.and_then(|m| m.get("hooks"))
.and_then(|h| h.get("post"))
.and_then(|p| serde_json::from_value::<Vec<CowHook>>(p.clone()).ok())
.unwrap_or_default()
}
#[must_use]
pub fn hash_quote(
dapp_id: &str,
buy_chain_id: u64,
sell_chain_id: u64,
token_address: &str,
) -> String {
format!("{dapp_id}-{buy_chain_id}-{sell_chain_id}-{}", token_address.to_lowercase())
}
#[must_use]
pub fn priority_stablecoin_tokens() -> HashMap<u64, HashSet<Address>> {
let mut map = HashMap::default();
let mut insert_chain = |chain_id: u64, addrs: &[&str]| {
let set: HashSet<Address> =
addrs.iter().filter_map(|a| a.parse::<Address>().ok()).collect();
map.insert(chain_id, set);
};
insert_chain(
SupportedChainId::Mainnet.as_u64(),
&[
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "0xdac17f958d2ee523a2206206994597c13d831ec7", ],
);
insert_chain(
SupportedChainId::BnbChain.as_u64(),
&[
"0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", "0x55d398326f99059ff775485246999027b3197955", ],
);
insert_chain(
SupportedChainId::GnosisChain.as_u64(),
&[
"0xddafbb505ad214d7b80b1f830fccc89b60fb7a83", "0x2a22f9c3b484c3629090feed35f17ff8f88f76f0", "0x4ecaba5870353805a9f068101a40e0f32ed605c6", ],
);
insert_chain(
SupportedChainId::Polygon.as_u64(),
&[
"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", ],
);
insert_chain(
SupportedChainId::Base.as_u64(),
&[
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "0xfde4c96c8593536e31f229ea8f37b2ada2699bb2", ],
);
insert_chain(
SupportedChainId::ArbitrumOne.as_u64(),
&[
"0xaf88d065e77c8cc2239327c5edb3a432268e5831", "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9", ],
);
insert_chain(
SupportedChainId::Avalanche.as_u64(),
&[
"0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e", "0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7", ],
);
insert_chain(
SupportedChainId::Linea.as_u64(),
&[
"0x176211869ca2b568f2a7d4ee941e073a821ee1ff", "0xa219439258ca9da29e9cc4ce5596924745e12b93", ],
);
insert_chain(
SupportedChainId::Sepolia.as_u64(),
&[
"0x1c7d4b196cb0c7b01d743fbc6116a902379c7238", ],
);
map
}
#[must_use]
pub fn is_stablecoin_priority_token(chain_id: u64, token_address: Address) -> bool {
let registry = priority_stablecoin_tokens();
registry.get(&chain_id).is_some_and(|set| set.contains(&token_address))
}
#[must_use]
pub fn is_correlated_token(token_address: Address, correlated_tokens: &HashSet<Address>) -> bool {
correlated_tokens.contains(&token_address)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub enum TokenPriority {
Lowest = 1,
Low = 2,
Medium = 3,
High = 4,
Highest = 5,
}
pub fn determine_intermediate_token(
source_chain_id: u64,
source_token_address: Address,
candidates: &[Address],
correlated_tokens: &HashSet<Address>,
allow_intermediate_eq_sell: bool,
) -> Result<Address, BridgeError> {
if candidates.is_empty() {
return Err(BridgeError::NoIntermediateTokens);
}
if candidates.len() == 1 {
return Ok(candidates[0]);
}
let native = cow_chains::NATIVE_CURRENCY_ADDRESS;
let filtered: Vec<Address> = if allow_intermediate_eq_sell {
candidates.to_vec()
} else {
candidates.iter().copied().filter(|a| *a != source_token_address).collect()
};
if filtered.is_empty() {
return Err(BridgeError::NoIntermediateTokens);
}
let is_sell_native = source_token_address == native;
let mut scored: Vec<(Address, TokenPriority)> = filtered
.iter()
.map(|&addr| {
let is_native = addr == native;
if addr == source_token_address && !(is_sell_native && is_native) {
return (addr, TokenPriority::Highest);
}
if is_stablecoin_priority_token(source_chain_id, addr) {
return (addr, TokenPriority::High);
}
if is_correlated_token(addr, correlated_tokens) {
return (addr, TokenPriority::Medium);
}
if is_native && !is_sell_native {
return (addr, TokenPriority::Low);
}
(addr, TokenPriority::Lowest)
})
.collect();
scored.sort_unstable_by_key(|item| std::cmp::Reverse(item.1));
scored.first().map(|(addr, _)| *addr).ok_or(BridgeError::NoIntermediateTokens)
}
pub type QueryParam = (String, Vec<String>);
#[must_use]
pub fn object_to_search_params(params: &[QueryParam]) -> String {
let pairs: Vec<String> = params
.iter()
.map(|(key, values)| {
let value = values.join(",");
format!("{key}={value}")
})
.collect();
pairs.join("&")
}
pub const fn validate_cross_chain_request(
sell_chain_id: u64,
buy_chain_id: u64,
) -> Result<(), BridgeError> {
if sell_chain_id == buy_chain_id {
return Err(BridgeError::SameChain);
}
Ok(())
}
#[must_use]
pub fn find_bridge_provider_dapp_id(full_app_data: &str) -> Option<String> {
let value: serde_json::Value = serde_json::from_str(full_app_data).ok()?;
if let Some(provider_id) = value
.get("metadata")
.and_then(|m| m.get("bridging"))
.and_then(|b| b.get("providerId"))
.and_then(|p| p.as_str())
{
return Some(provider_id.to_owned());
}
let post_hooks = get_post_hooks(full_app_data);
post_hooks
.into_iter()
.find_map(|hook| hook.dapp_id.filter(|id| id.starts_with(HOOK_DAPP_BRIDGE_PROVIDER_PREFIX)))
}
#[must_use]
pub fn is_better_quote(
candidate: &MultiQuoteResult,
current_best: Option<&MultiQuoteResult>,
) -> bool {
let Some(best) = current_best else {
return candidate.quote.is_some();
};
match (&candidate.quote, &best.quote) {
(Some(c), Some(b)) => c.after_slippage.buy_amount > b.after_slippage.buy_amount,
(Some(_), None) => true,
_ => false,
}
}
#[must_use]
pub const fn is_better_error(
candidate: Option<&BridgeError>,
current_best: Option<&BridgeError>,
) -> bool {
match (candidate, current_best) {
(Some(c), Some(b)) => {
crate::types::bridge_error_priority(c) > crate::types::bridge_error_priority(b)
}
(Some(_), None) => true,
_ => false,
}
}
pub fn fill_timeout_results(results: &mut Vec<MultiQuoteResult>, provider_dapp_ids: &[String]) {
for (i, dapp_id) in provider_dapp_ids.iter().enumerate() {
if i >= results.len() || results[i].quote.is_none() && results[i].error.is_none() {
if i < results.len() {
results[i] = MultiQuoteResult {
provider_dapp_id: dapp_id.clone(),
quote: None,
error: Some("provider request timed out".to_owned()),
};
} else {
results.push(MultiQuoteResult {
provider_dapp_id: dapp_id.clone(),
quote: None,
error: Some("provider request timed out".to_owned()),
});
}
}
}
}
pub const COW_SHED_PROXY_CREATION_GAS: u64 = 360_000;
#[must_use]
pub fn get_gas_limit_estimation_for_hook(
proxy_deployed: bool,
extra_gas: Option<u64>,
extra_gas_proxy_creation: Option<u64>,
) -> u64 {
use super::sdk::DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION;
if proxy_deployed {
DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION + extra_gas.map_or(0, |v| v)
} else {
let base = DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION + COW_SHED_PROXY_CREATION_GAS;
base + extra_gas_proxy_creation.map_or(0, |v| v)
}
}
#[must_use]
pub const fn is_infrastructure_error(status: u16) -> bool {
status >= 500 || status == 429
}
#[must_use]
pub fn is_client_fetch_error(error: &str) -> bool {
error.contains("fetch") || error.contains("network") || error.contains("dns")
}
#[must_use]
pub fn is_app_doc(json_str: &str) -> bool {
let Ok(value) = serde_json::from_str::<serde_json::Value>(json_str) else {
return false;
};
value.is_object() && value.get("version").is_some() && value.get("metadata").is_some()
}
pub fn resolve_providers_to_query<'a>(
dapp_ids: Option<&[String]>,
available_dapp_ids: &'a [String],
) -> Result<Vec<&'a str>, BridgeError> {
let Some(requested) = dapp_ids else {
return Ok(available_dapp_ids.iter().map(String::as_str).collect());
};
requested
.iter()
.map(|id| {
available_dapp_ids
.iter()
.find(|avail| avail.as_str() == id.as_str())
.map(String::as_str)
.ok_or_else(|| BridgeError::ProviderNotFound { dapp_id: id.clone() })
})
.collect()
}
pub use find_bridge_provider_dapp_id as find_bridge_provider_from_hook;
pub use hook_mock_for_cost_estimation as get_hook_mock_for_cost_estimation;
use super::across::{across_token_mapping, get_token_address, get_token_symbol};
#[must_use]
pub fn adapt_token(
token_address: Address,
source_chain_id: u64,
target_chain_id: u64,
) -> Option<Address> {
let mapping = across_token_mapping();
let source_config = mapping.get(&source_chain_id)?;
let symbol = get_token_symbol(token_address, source_config)?;
let target_config = mapping.get(&target_chain_id)?;
get_token_address(&symbol, target_config)
}
#[must_use]
pub fn adapt_tokens(
tokens: &[Address],
source_chain_id: u64,
target_chain_id: u64,
) -> Vec<Address> {
tokens.iter().filter_map(|&addr| adapt_token(addr, source_chain_id, target_chain_id)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{BridgeAmounts, BridgeCosts, BridgeQuoteAmountsAndCosts, BridgingFee};
#[test]
fn apply_bps_zero_bps_returns_full_amount() {
assert_eq!(apply_bps(U256::from(10_000u64), 0), U256::from(10_000u64));
}
#[test]
fn apply_bps_50_bps() {
assert_eq!(apply_bps(U256::from(10_000u64), 50), U256::from(9_950u64));
}
#[test]
fn apply_bps_full_10000_bps_returns_zero() {
assert_eq!(apply_bps(U256::from(10_000u64), 10_000), U256::ZERO);
}
#[test]
fn apply_pct_fee_zero_fee() {
let result = apply_pct_fee(U256::from(1_000_000u64), 0).unwrap();
assert_eq!(result, U256::from(1_000_000u64));
}
#[test]
fn apply_pct_fee_50_percent() {
let pct = 500_000_000_000_000_000u128;
let result = apply_pct_fee(U256::from(1_000_000u64), pct).unwrap();
assert_eq!(result, U256::from(500_000u64));
}
#[test]
fn apply_pct_fee_exceeds_100_percent() {
let pct = 10u128.pow(18) + 1;
assert!(apply_pct_fee(U256::from(1_000u64), pct).is_err());
}
#[test]
fn pct_to_bps_zero() {
assert_eq!(pct_to_bps(0).unwrap(), 0);
}
#[test]
fn pct_to_bps_100_percent() {
assert_eq!(pct_to_bps(10u128.pow(18)).unwrap(), 10_000);
}
#[test]
fn pct_to_bps_1_percent() {
assert_eq!(pct_to_bps(10u128.pow(16)).unwrap(), 100);
}
#[test]
fn pct_to_bps_exceeds_100_percent() {
assert!(pct_to_bps(10u128.pow(18) + 1).is_err());
}
#[test]
fn calculate_fee_bps_basic() {
let bps = calculate_fee_bps(U256::from(50u64), U256::from(10_000u64)).unwrap();
assert_eq!(bps, 50);
}
#[test]
fn calculate_fee_bps_zero_fee() {
let bps = calculate_fee_bps(U256::ZERO, U256::from(10_000u64)).unwrap();
assert_eq!(bps, 0);
}
#[test]
fn calculate_fee_bps_zero_amount_errors() {
assert!(calculate_fee_bps(U256::from(1u64), U256::ZERO).is_err());
}
#[test]
fn calculate_fee_bps_fee_exceeds_amount_errors() {
assert!(calculate_fee_bps(U256::from(200u64), U256::from(100u64)).is_err());
}
#[test]
fn calculate_deadline_basic() {
assert_eq!(calculate_deadline(1_700_000_000, 300), 1_700_000_300);
}
#[test]
fn calculate_deadline_saturates() {
assert_eq!(calculate_deadline(u64::MAX, 1), u64::MAX);
}
#[test]
fn are_hooks_equal_identical() {
let hook = CowHook {
call_data: "0xabc".to_owned(),
gas_limit: "100000".to_owned(),
target: "0x1111111111111111111111111111111111111111".to_owned(),
dapp_id: None,
};
assert!(are_hooks_equal(&hook, &hook));
}
#[test]
fn are_hooks_equal_different_call_data() {
let a = CowHook {
call_data: "0xabc".to_owned(),
gas_limit: "100000".to_owned(),
target: "0x1111111111111111111111111111111111111111".to_owned(),
dapp_id: None,
};
let b = CowHook {
call_data: "0xdef".to_owned(),
gas_limit: "100000".to_owned(),
target: "0x1111111111111111111111111111111111111111".to_owned(),
dapp_id: None,
};
assert!(!are_hooks_equal(&a, &b));
}
#[test]
fn are_hooks_equal_ignores_dapp_id() {
let a = CowHook {
call_data: "0xabc".to_owned(),
gas_limit: "100000".to_owned(),
target: "0x1111111111111111111111111111111111111111".to_owned(),
dapp_id: Some("a".to_owned()),
};
let b = CowHook {
call_data: "0xabc".to_owned(),
gas_limit: "100000".to_owned(),
target: "0x1111111111111111111111111111111111111111".to_owned(),
dapp_id: Some("b".to_owned()),
};
assert!(are_hooks_equal(&a, &b));
}
#[test]
fn hook_mock_for_cost_estimation_has_expected_fields() {
let hook = hook_mock_for_cost_estimation(200_000);
assert_eq!(hook.call_data, "0x00");
assert_eq!(hook.gas_limit, "200000");
assert_eq!(hook.target, "0x0000000000000000000000000000000000000000");
assert!(hook.dapp_id.is_some());
}
#[test]
fn get_post_hooks_valid_json() {
let json = r#"{"version":"1.0","metadata":{"hooks":{"post":[{"target":"0x0000000000000000000000000000000000000000","callData":"0x00","gasLimit":"100000"}]}}}"#;
let hooks = get_post_hooks(json);
assert_eq!(hooks.len(), 1);
}
#[test]
fn get_post_hooks_invalid_json() {
assert!(get_post_hooks("not json").is_empty());
}
#[test]
fn get_post_hooks_no_hooks_key() {
assert!(get_post_hooks(r#"{"version":"1.0","metadata":{}}"#).is_empty());
}
#[test]
fn hash_quote_deterministic() {
let h1 = hash_quote("provider", 1, 42161, "0xABC");
let h2 = hash_quote("provider", 1, 42161, "0xABC");
assert_eq!(h1, h2);
}
#[test]
fn hash_quote_lowercases_token() {
let h1 = hash_quote("provider", 1, 42161, "0xABC");
let h2 = hash_quote("provider", 1, 42161, "0xabc");
assert_eq!(h1, h2);
}
#[test]
fn object_to_search_params_single_value() {
let params = vec![("key".to_owned(), vec!["value".to_owned()])];
assert_eq!(object_to_search_params(¶ms), "key=value");
}
#[test]
fn object_to_search_params_multiple_values() {
let params = vec![("bridges".to_owned(), vec!["across".to_owned(), "cctp".to_owned()])];
assert_eq!(object_to_search_params(¶ms), "bridges=across,cctp");
}
#[test]
fn object_to_search_params_empty() {
assert_eq!(object_to_search_params(&[]), "");
}
#[test]
fn validate_cross_chain_request_different_chains_ok() {
assert!(validate_cross_chain_request(1, 42161).is_ok());
}
#[test]
fn validate_cross_chain_request_same_chain_err() {
assert!(validate_cross_chain_request(1, 1).is_err());
}
#[test]
fn find_bridge_provider_from_metadata() {
let json = r#"{"version":"1.0","metadata":{"bridging":{"providerId":"my-provider"}}}"#;
assert_eq!(find_bridge_provider_dapp_id(json), Some("my-provider".to_owned()));
}
#[test]
fn find_bridge_provider_from_post_hooks() {
let dapp_id = format!("{HOOK_DAPP_BRIDGE_PROVIDER_PREFIX}/test");
let json = format!(
r#"{{"version":"1.0","metadata":{{"hooks":{{"post":[{{"target":"0x0000000000000000000000000000000000000000","callData":"0x00","gasLimit":"100000","dappId":"{dapp_id}"}}]}}}}}}"#
);
assert_eq!(find_bridge_provider_dapp_id(&json), Some(dapp_id));
}
#[test]
fn find_bridge_provider_none_on_invalid_json() {
assert!(find_bridge_provider_dapp_id("not json").is_none());
}
#[test]
fn is_better_quote_none_best_with_some_candidate() {
let candidate = MultiQuoteResult {
provider_dapp_id: "a".into(),
quote: Some(BridgeQuoteAmountsAndCosts {
costs: BridgeCosts {
bridging_fee: BridgingFee {
fee_bps: 0,
amount_in_sell_currency: U256::ZERO,
amount_in_buy_currency: U256::ZERO,
},
},
before_fee: BridgeAmounts {
sell_amount: U256::from(100u64),
buy_amount: U256::from(100u64),
},
after_fee: BridgeAmounts {
sell_amount: U256::from(100u64),
buy_amount: U256::from(100u64),
},
after_slippage: BridgeAmounts {
sell_amount: U256::from(100u64),
buy_amount: U256::from(100u64),
},
slippage_bps: 0,
}),
error: None,
};
assert!(is_better_quote(&candidate, None));
}
#[test]
fn is_better_quote_no_quote_candidate_returns_false() {
let candidate = MultiQuoteResult {
provider_dapp_id: "a".into(),
quote: None,
error: Some("err".into()),
};
assert!(!is_better_quote(&candidate, None));
}
#[test]
fn is_better_error_some_vs_none() {
assert!(is_better_error(Some(&BridgeError::SameChain), None));
}
#[test]
fn is_better_error_none_vs_some() {
assert!(!is_better_error(None, Some(&BridgeError::SameChain)));
}
#[test]
fn is_better_error_higher_priority_wins() {
assert!(is_better_error(
Some(&BridgeError::SellAmountTooSmall),
Some(&BridgeError::SameChain),
));
}
#[test]
fn fill_timeout_results_fills_missing_entries() {
let mut results = vec![];
let providers = vec!["p1".to_owned(), "p2".to_owned()];
fill_timeout_results(&mut results, &providers);
assert_eq!(results.len(), 2);
assert!(results[0].error.as_deref() == Some("provider request timed out"));
assert!(results[1].error.as_deref() == Some("provider request timed out"));
}
#[test]
fn fill_timeout_results_leaves_existing_entries() {
let mut results = vec![MultiQuoteResult {
provider_dapp_id: "p1".into(),
quote: None,
error: Some("custom error".into()),
}];
let providers = vec!["p1".to_owned()];
fill_timeout_results(&mut results, &providers);
assert_eq!(results[0].error.as_deref(), Some("custom error"));
}
#[test]
fn gas_limit_proxy_deployed() {
use super::super::sdk::DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION;
let result = get_gas_limit_estimation_for_hook(true, None, None);
assert_eq!(result, DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION);
}
#[test]
fn gas_limit_proxy_not_deployed() {
use super::super::sdk::DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION;
let result = get_gas_limit_estimation_for_hook(false, None, None);
assert_eq!(result, DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION + COW_SHED_PROXY_CREATION_GAS);
}
#[test]
fn gas_limit_with_extra_gas() {
use super::super::sdk::DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION;
let result = get_gas_limit_estimation_for_hook(true, Some(50_000), None);
assert_eq!(result, DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION + 50_000);
}
#[test]
fn gas_limit_not_deployed_with_extra_proxy_gas() {
use super::super::sdk::DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION;
let result = get_gas_limit_estimation_for_hook(false, None, Some(10_000));
assert_eq!(
result,
DEFAULT_GAS_COST_FOR_HOOK_ESTIMATION + COW_SHED_PROXY_CREATION_GAS + 10_000
);
}
#[test]
fn is_infrastructure_error_500() {
assert!(is_infrastructure_error(500));
assert!(is_infrastructure_error(503));
}
#[test]
fn is_infrastructure_error_429() {
assert!(is_infrastructure_error(429));
}
#[test]
fn is_infrastructure_error_200() {
assert!(!is_infrastructure_error(200));
}
#[test]
fn is_infrastructure_error_404() {
assert!(!is_infrastructure_error(404));
}
#[test]
fn is_client_fetch_error_matches() {
assert!(is_client_fetch_error("fetch failed"));
assert!(is_client_fetch_error("network error"));
assert!(is_client_fetch_error("dns resolution failed"));
}
#[test]
fn is_client_fetch_error_no_match() {
assert!(!is_client_fetch_error("timeout"));
assert!(!is_client_fetch_error("internal error"));
}
#[test]
fn is_app_doc_valid() {
assert!(is_app_doc(r#"{"version":"1.0","metadata":{}}"#));
}
#[test]
fn is_app_doc_missing_version() {
assert!(!is_app_doc(r#"{"metadata":{}}"#));
}
#[test]
fn is_app_doc_invalid_json() {
assert!(!is_app_doc("nope"));
}
#[test]
fn resolve_providers_none_returns_all() {
let available = vec!["a".to_owned(), "b".to_owned()];
let result = resolve_providers_to_query(None, &available).unwrap();
assert_eq!(result, vec!["a", "b"]);
}
#[test]
fn resolve_providers_specific_ids() {
let available = vec!["a".to_owned(), "b".to_owned(), "c".to_owned()];
let requested = vec!["b".to_owned()];
let result = resolve_providers_to_query(Some(&requested), &available).unwrap();
assert_eq!(result, vec!["b"]);
}
#[test]
fn resolve_providers_missing_id_errors() {
let available = vec!["a".to_owned()];
let requested = vec!["z".to_owned()];
assert!(resolve_providers_to_query(Some(&requested), &available).is_err());
}
#[test]
fn priority_stablecoin_tokens_includes_mainnet() {
let map = priority_stablecoin_tokens();
let mainnet = map.get(&1).unwrap();
let usdc: Address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".parse().unwrap();
assert!(mainnet.contains(&usdc));
}
#[test]
fn is_stablecoin_mainnet_usdc() {
let usdc: Address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".parse().unwrap();
assert!(is_stablecoin_priority_token(1, usdc));
}
#[test]
fn is_stablecoin_unknown_chain() {
let usdc: Address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".parse().unwrap();
assert!(!is_stablecoin_priority_token(999_999, usdc));
}
#[test]
fn is_correlated_token_found() {
let addr: Address = "0x1111111111111111111111111111111111111111".parse().unwrap();
let mut set = HashSet::default();
set.insert(addr);
assert!(is_correlated_token(addr, &set));
}
#[test]
fn is_correlated_token_not_found() {
let addr: Address = "0x1111111111111111111111111111111111111111".parse().unwrap();
let set = HashSet::default();
assert!(!is_correlated_token(addr, &set));
}
#[test]
fn determine_intermediate_token_empty_candidates_errors() {
let correlated = HashSet::default();
assert!(determine_intermediate_token(1, Address::ZERO, &[], &correlated, false).is_err());
}
#[test]
fn determine_intermediate_token_single_candidate() {
let addr: Address = "0x1111111111111111111111111111111111111111".parse().unwrap();
let correlated = HashSet::default();
let result = determine_intermediate_token(1, Address::ZERO, &[addr], &correlated, false);
assert_eq!(result.unwrap(), addr);
}
#[test]
fn determine_intermediate_token_prefers_stablecoin() {
let usdc: Address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".parse().unwrap();
let random: Address = "0x1111111111111111111111111111111111111111".parse().unwrap();
let correlated = HashSet::default();
let result =
determine_intermediate_token(1, Address::ZERO, &[random, usdc], &correlated, false);
assert_eq!(result.unwrap(), usdc);
}
#[test]
fn determine_intermediate_token_prefers_same_token() {
let sell: Address = "0x2222222222222222222222222222222222222222".parse().unwrap();
let usdc: Address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".parse().unwrap();
let correlated = HashSet::default();
let result = determine_intermediate_token(1, sell, &[usdc, sell], &correlated, true);
assert_eq!(result.unwrap(), sell);
}
#[test]
fn adapt_token_mainnet_usdc_to_arbitrum() {
let usdc: Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".parse().unwrap();
assert!(adapt_token(usdc, 1, 42161).is_some());
}
#[test]
fn adapt_token_unknown_chain_returns_none() {
assert!(adapt_token(Address::ZERO, 999_999, 1).is_none());
}
#[test]
fn adapt_tokens_filters_unmapped() {
let usdc: Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".parse().unwrap();
let results = adapt_tokens(&[usdc, Address::ZERO], 1, 42161);
assert_eq!(results.len(), 1);
}
#[test]
fn token_priority_ordering() {
assert!(TokenPriority::Highest > TokenPriority::High);
assert!(TokenPriority::High > TokenPriority::Medium);
assert!(TokenPriority::Medium > TokenPriority::Low);
assert!(TokenPriority::Low > TokenPriority::Lowest);
}
}