use std::{collections::HashMap, sync::Arc};
use fynd_core::{
derived::{ComponentDepths, TokenGasPrices},
types::ComponentId,
};
use serde::{Deserialize, Serialize};
use tycho_simulation::{
tycho_common::models::{token::Token, Address},
tycho_core::simulation::protocol_sim::Price,
};
use utoipa::{IntoParams, ToSchema};
#[derive(Debug, Default, Deserialize, IntoParams)]
pub struct TokensQuery {
#[param(example = 1000)]
pub limit: Option<usize>,
#[param(example = 0)]
pub offset: Option<usize>,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct TokensResponse {
pub tokens: Vec<GraphTokenEntry>,
pub total: usize,
pub block: u64,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct GraphTokenEntry {
#[schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")]
pub address: Address,
pub symbol: String,
pub decimals: u32,
pub tax: u64,
pub gas: Vec<Option<u64>>,
pub quality: u32,
pub component_count: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub liquidity: Option<f64>,
}
#[derive(Debug, Clone)]
pub(crate) struct TokensCache {
pub key: (u64, Option<u64>),
pub entries: Arc<Vec<GraphTokenEntry>>,
}
fn price_ratio_f64(price: &Price) -> Option<f64> {
use num_traits::ToPrimitive;
let ratio = price.numerator.to_f64()? / price.denominator.to_f64()?;
(ratio.is_finite() && ratio > 0.0).then_some(ratio)
}
pub fn build_token_entries(
topology: &HashMap<ComponentId, Vec<Address>>,
token_registry: &HashMap<Address, Token>,
depths: Option<&ComponentDepths>,
token_prices: Option<&TokenGasPrices>,
) -> Vec<GraphTokenEntry> {
use num_traits::ToPrimitive;
let mut component_counts: HashMap<Address, u32> = HashMap::new();
for tokens in topology.values() {
for address in tokens {
*component_counts
.entry(address.clone())
.or_default() += 1;
}
}
let mut liquidity: HashMap<Address, f64> = HashMap::new();
if let (Some(depths), Some(prices)) = (depths, token_prices) {
for ((_, token_in, _), depth) in depths {
if !component_counts.contains_key(token_in) {
continue;
}
let Some(price) = prices.get(token_in) else { continue };
let Some(price) = price_ratio_f64(price) else { continue };
let Some(depth) = depth.to_f64() else { continue };
let gas_units = depth * price;
if gas_units.is_finite() {
*liquidity
.entry(token_in.clone())
.or_default() += gas_units;
}
}
}
let mut entries = Vec::with_capacity(component_counts.len());
for (address, component_count) in component_counts {
let Some(token) = token_registry.get(&address) else { continue };
entries.push(GraphTokenEntry {
symbol: token.symbol.clone(),
decimals: token.decimals,
tax: token.tax,
gas: token.gas.clone(),
quality: token.quality,
component_count,
liquidity: liquidity.get(&address).copied(),
address,
});
}
entries.sort_by(|a, b| {
b.liquidity
.unwrap_or(f64::NEG_INFINITY)
.total_cmp(&a.liquidity.unwrap_or(f64::NEG_INFINITY))
.then_with(|| {
b.component_count
.cmp(&a.component_count)
})
.then_with(|| a.address.cmp(&b.address))
});
entries
}
#[cfg(test)]
mod tests {
use num_bigint::BigUint;
use tycho_simulation::{
tycho_common::models::Chain, tycho_core::simulation::protocol_sim::Price,
};
use super::*;
fn addr(byte: u8) -> Address {
Address::from([byte; 20])
}
fn test_token(byte: u8, symbol: &str, decimals: u32) -> Token {
Token {
address: addr(byte),
symbol: symbol.to_string(),
decimals,
tax: 0,
gas: vec![],
chain: Chain::Ethereum,
quality: 100,
}
}
fn test_market() -> (HashMap<ComponentId, Vec<Address>>, HashMap<Address, Token>) {
let topology = HashMap::from([
("c1".to_string(), vec![addr(0x0a), addr(0x0b)]),
("c2".to_string(), vec![addr(0x0a), addr(0x0b)]),
("c3".to_string(), vec![addr(0x0a), addr(0x0c)]),
]);
let registry =
[test_token(0x0a, "AAA", 18), test_token(0x0b, "BBB", 6), test_token(0x0c, "CCC", 8)]
.into_iter()
.map(|t| (t.address.clone(), t))
.collect();
(topology, registry)
}
#[test]
fn test_build_token_entries_counts_components_without_derived_data() {
let (topology, registry) = test_market();
let entries = build_token_entries(&topology, ®istry, None, None);
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].address, addr(0x0a));
assert_eq!(entries[0].component_count, 3);
assert_eq!(entries[0].liquidity, None);
assert_eq!(entries[1].component_count, 2);
assert_eq!(entries[2].component_count, 1);
}
#[test]
fn test_build_token_entries_sums_depths_into_gas_units() {
let (topology, registry) = test_market();
let depths: ComponentDepths = [
(("c1".to_string(), addr(0x0a), addr(0x0b)), BigUint::from(100u32)),
(("c3".to_string(), addr(0x0a), addr(0x0c)), BigUint::from(300u32)),
(("c1".to_string(), addr(0x0b), addr(0x0a)), BigUint::from(50u32)),
]
.into_iter()
.collect();
let prices: TokenGasPrices = [
(addr(0x0a), Price::new(BigUint::from(2u8), BigUint::from(1u8))),
(addr(0x0b), Price::new(BigUint::from(1u8), BigUint::from(1u8))),
]
.into_iter()
.collect();
let entries = build_token_entries(&topology, ®istry, Some(&depths), Some(&prices));
let entry_a = entries
.iter()
.find(|e| e.address == addr(0x0a))
.unwrap();
let entry_b = entries
.iter()
.find(|e| e.address == addr(0x0b))
.unwrap();
let entry_c = entries
.iter()
.find(|e| e.address == addr(0x0c))
.unwrap();
assert_eq!(entry_a.liquidity, Some(800.0));
assert_eq!(entry_b.liquidity, Some(50.0));
assert_eq!(entry_c.liquidity, None);
assert_eq!(entries[0].address, addr(0x0a));
assert_eq!(entries[1].address, addr(0x0b));
assert_eq!(entries[2].address, addr(0x0c));
}
#[test]
fn test_build_token_entries_skips_tokens_missing_from_registry() {
let (topology, mut registry) = test_market();
registry.remove(&addr(0x0c));
let entries = build_token_entries(&topology, ®istry, None, None);
assert_eq!(entries.len(), 2);
assert!(entries
.iter()
.all(|e| e.address != addr(0x0c)));
}
#[test]
fn test_build_token_entries_ignores_depths_for_tokens_outside_topology() {
let (topology, registry) = test_market();
let depths: ComponentDepths =
[(("cx".to_string(), addr(0xff), addr(0x0a)), BigUint::from(500u32))]
.into_iter()
.collect();
let prices: TokenGasPrices =
[(addr(0xff), Price::new(BigUint::from(1u8), BigUint::from(1u8)))]
.into_iter()
.collect();
let entries = build_token_entries(&topology, ®istry, Some(&depths), Some(&prices));
assert_eq!(entries.len(), 3);
assert!(entries
.iter()
.all(|e| e.liquidity.is_none()));
}
#[test]
fn test_build_token_entries_empty_topology() {
let (_, registry) = test_market();
assert!(build_token_entries(&HashMap::new(), ®istry, None, None).is_empty());
}
}