use std::collections::HashMap;
use crate::error::{FinanceError, Result};
use crate::models::crypto::defi::{ChainAllocation, ProtocolTvl, TvlPoint};
use super::models::{LiteProtocol, LiteResponse, Period, ProtocolResponse};
pub(super) fn slug(id: &str) -> String {
id.trim()
.to_lowercase()
.replace([' ', '_'], "-")
.replace('\'', "")
}
const EXCLUDE_PARENT: &str = "excludeParent";
const BREAKDOWN_KEYS: [&str; 10] = [
"borrowed",
"dcAndLsOverlap",
"doublecounted",
EXCLUDE_PARENT,
"liquidstaking",
"offers",
"pool2",
"staking",
"treasury",
"vesting",
];
fn is_breakdown_key(key: &str) -> bool {
BREAKDOWN_KEYS.contains(&key)
|| key
.rsplit_once('-')
.is_some_and(|(_, suffix)| BREAKDOWN_KEYS.contains(&suffix))
}
pub(super) fn chain_allocations(
chains: &[String],
chain_tvls: &HashMap<String, f64>,
) -> Vec<ChainAllocation> {
let named = !chains.is_empty();
let mut out: Vec<ChainAllocation> = chain_tvls
.iter()
.filter(|(key, _)| match named {
true => chains.iter().any(|chain| chain == *key),
false => !is_breakdown_key(key),
})
.map(|(chain, tvl)| ChainAllocation {
chain: chain.clone(),
tvl: *tvl,
})
.collect();
out.sort_by(|a, b| b.tvl.total_cmp(&a.tvl).then_with(|| a.chain.cmp(&b.chain)));
out
}
pub(super) fn change_percent(current: Option<f64>, previous: Option<f64>) -> Option<f64> {
let (current, previous) = (current?, previous?);
if previous == 0.0 {
return None;
}
Some((current - previous) / previous * 100.0)
}
fn sum_present(values: impl Iterator<Item = Option<f64>>) -> Option<f64> {
values
.flatten()
.fold(None, |acc, v| Some(acc.unwrap_or(0.0) + v))
}
struct Resolved<'a> {
name: Option<&'a str>,
symbol: Option<&'a str>,
url: Option<&'a str>,
chains: &'a [String],
mcap: Option<f64>,
members: Vec<&'a LiteProtocol>,
in_parent: bool,
}
impl Resolved<'_> {
fn excluded_share(&self, member: &LiteProtocol, key: &str, period: Period) -> f64 {
match self.in_parent {
true => member
.chain_tvls
.get(key)
.and_then(|entry| entry.tvl_at(period))
.unwrap_or(0.0),
false => 0.0,
}
}
fn member_tvl(&self, member: &LiteProtocol, period: Period) -> Option<f64> {
Some(member.tvl_at(period)? - self.excluded_share(member, EXCLUDE_PARENT, period))
}
fn tvl(&self) -> Option<f64> {
sum_present(self.members.iter().map(|m| self.member_tvl(m, Period::Now)))
}
fn change(&self, period: Period) -> Option<f64> {
let (current, before) = self
.members
.iter()
.filter_map(|m| {
Some((
self.member_tvl(m, Period::Now)?,
self.member_tvl(m, period)?,
))
})
.fold(None, |acc, (now, then)| {
let (current, before) = acc.unwrap_or((0.0, 0.0));
Some((current + now, before + then))
})?;
change_percent(Some(current), Some(before))
}
fn chain_tvls(&self) -> HashMap<String, f64> {
let mut totals: HashMap<String, f64> = HashMap::new();
for member in &self.members {
for (key, entry) in &member.chain_tvls {
if is_breakdown_key(key) {
continue;
}
let excluded =
self.excluded_share(member, &format!("{key}-{EXCLUDE_PARENT}"), Period::Now);
*totals.entry(key.clone()).or_default() += entry.tvl - excluded;
}
}
totals
}
}
fn resolve<'a>(list: &'a LiteResponse, id: &str) -> Option<Resolved<'a>> {
if let Some(protocol) = list.protocols.iter().find(|p| slug(&p.name) == id) {
return Some(Resolved {
name: Some(&protocol.name),
symbol: protocol.symbol.as_deref(),
url: protocol.url.as_deref(),
chains: &protocol.chains,
mcap: protocol.mcap,
members: vec![protocol],
in_parent: false,
});
}
let parent = list.parent_protocols.iter().find(|parent| {
parent
.id
.strip_prefix("parent#")
.is_some_and(|suffix| slug(suffix) == id)
})?;
Some(Resolved {
name: parent.name.as_deref(),
symbol: parent.symbol.as_deref(),
url: parent.url.as_deref(),
chains: &parent.chains,
mcap: parent.mcap,
members: list
.protocols
.iter()
.filter(|p| p.parent_protocol.as_deref() == Some(parent.id.as_str()))
.filter(|p| !p.exclude_tvl_from_parent && !p.deprecated)
.collect(),
in_parent: true,
})
}
pub(super) fn to_history(response: &ProtocolResponse) -> Vec<TvlPoint> {
let mut points: Vec<TvlPoint> = response
.tvl
.iter()
.map(|snapshot| TvlPoint {
timestamp: snapshot.date,
tvl: snapshot.total_liquidity_usd,
})
.collect();
points.sort_by_key(|point| point.timestamp);
points
}
pub(super) fn to_protocol_tvl(slug: &str, list: &LiteResponse) -> Result<ProtocolTvl> {
let resolved = resolve(list, slug).ok_or_else(|| FinanceError::SymbolNotFound {
symbol: Some(slug.to_string()),
context: "DefiLlama knows no protocol by this slug".to_string(),
})?;
let tvl_by_chain = chain_allocations(resolved.chains, &resolved.chain_tvls());
let chains = match resolved.chains.is_empty() {
true => tvl_by_chain.iter().map(|a| a.chain.clone()).collect(),
false => resolved.chains.to_vec(),
};
Ok(ProtocolTvl {
slug: slug.to_string(),
name: resolved.name.map(str::to_string),
symbol: resolved.symbol.map(str::to_string),
url: resolved.url.map(str::to_string),
chains,
tvl: resolved.tvl(),
tvl_by_chain,
change_1d_percent: resolved.change(Period::Day),
change_7d_percent: resolved.change(Period::Week),
market_cap: resolved.mcap,
})
}
pub(crate) async fn fetch_protocol_tvl_response(protocol: &str) -> Result<ProtocolTvl> {
let slug = slug(protocol);
let list = super::client()?.lite_protocols().await?;
to_protocol_tvl(&slug, &list)
}
pub(crate) async fn fetch_protocol_tvl_history_response(protocol: &str) -> Result<Vec<TvlPoint>> {
let response = super::client()?.protocol(&slug(protocol)).await?;
Ok(to_history(&response))
}