use crate::error::Result;
use crate::models::crypto::defi::{ChainAllocation, ProtocolTvl, TvlPoint};
use super::models::ProtocolResponse;
const DAY: i64 = 86_400;
pub(super) fn slug(id: &str) -> String {
id.trim().to_lowercase().replace([' ', '_'], "-")
}
const BREAKDOWN_KEYS: [&str; 5] = ["borrowed", "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(response: &ProtocolResponse) -> Vec<ChainAllocation> {
let named = !response.chains.is_empty();
let mut out: Vec<ChainAllocation> = response
.current_chain_tvls
.iter()
.filter(|(key, _)| match named {
true => response.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(history: &[TvlPoint], days_ago: i64) -> Option<f64> {
let latest = history.last()?;
let cutoff = latest.timestamp - days_ago * DAY;
let past = history
.iter()
.rev()
.find(|point| point.timestamp <= cutoff)?;
if past.tvl == 0.0 {
return None;
}
Some((latest.tvl - past.tvl) / past.tvl * 100.0)
}
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, response: &ProtocolResponse) -> ProtocolTvl {
let history = to_history(response);
let tvl_by_chain = chain_allocations(response);
let chains = match response.chains.is_empty() {
true => tvl_by_chain.iter().map(|a| a.chain.clone()).collect(),
false => response.chains.clone(),
};
ProtocolTvl {
slug: slug.to_string(),
name: response.name.clone(),
symbol: response.symbol.clone(),
url: response.url.clone(),
chains,
tvl: history.last().map(|point| point.tvl),
tvl_by_chain,
change_1d_percent: change_percent(&history, 1),
change_7d_percent: change_percent(&history, 7),
market_cap: response.mcap,
}
}
pub(crate) async fn fetch_protocol_tvl_response(protocol: &str) -> Result<ProtocolTvl> {
let slug = slug(protocol);
let response = super::client()?.protocol(&slug).await?;
Ok(to_protocol_tvl(&slug, &response))
}
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))
}