use std::sync::Arc;
use std::time::Duration;
use reqwest::{Client, StatusCode};
use tracing::debug;
use super::models::{ChainResponse, ProtocolResponse, StablecoinsResponse};
use crate::adapters::common::{keyless_http_client, status_error};
use crate::error::{FinanceError, Result};
use crate::rate_limiter::RateLimiter;
pub(super) const LLAMA_BASE: &str = "https://api.llama.fi";
pub(super) const STABLECOINS_BASE: &str = "https://stablecoins.llama.fi";
pub(super) struct DefiLlamaClient {
http: Client,
limiter: Arc<RateLimiter>,
base_url: String,
stablecoins_url: String,
}
impl DefiLlamaClient {
pub(super) fn new(
timeout: Duration,
limiter: Arc<RateLimiter>,
base_url: impl Into<String>,
stablecoins_url: impl Into<String>,
) -> Result<Self> {
Ok(Self {
http: keyless_http_client(timeout)?,
limiter,
base_url: base_url.into(),
stablecoins_url: stablecoins_url.into(),
})
}
pub(super) async fn protocol(&self, slug: &str) -> Result<ProtocolResponse> {
let url = format!(
"{}/protocol/{}",
self.base_url,
crate::adapters::common::encode_path_segment(slug)
);
self.get(&url, Some(slug)).await
}
pub(super) async fn chains(&self) -> Result<Vec<ChainResponse>> {
let url = format!("{}/v2/chains", self.base_url);
self.get(&url, None).await
}
pub(super) async fn stablecoins(&self) -> Result<StablecoinsResponse> {
let url = format!("{}/stablecoins?includePrices=false", self.stablecoins_url);
self.get(&url, None).await
}
async fn get<T: serde::de::DeserializeOwned>(
&self,
url: &str,
subject: Option<&str>,
) -> Result<T> {
self.limiter.acquire().await;
debug!("DefiLlama request: {url}");
let resp = self.http.get(url).send().await?;
let status = resp.status();
let bytes = resp.bytes().await?;
if !status.is_success() {
return Err(match status {
StatusCode::BAD_REQUEST | StatusCode::NOT_FOUND => match subject {
Some(slug) => FinanceError::SymbolNotFound {
symbol: Some(slug.to_string()),
context: "DefiLlama knows no protocol by this slug".to_string(),
},
None => status_error("DefiLlama", status),
},
s => status_error("DefiLlama", s),
});
}
serde_json::from_slice(&bytes).map_err(|e| FinanceError::ResponseStructureError {
field: "defillama.response".to_string(),
context: format!("unrecognised DefiLlama payload: {e}"),
})
}
}