use crate::analytics::AnalyticsApi;
use crate::block::BlockApi;
use crate::defi::DefiApi;
use crate::discovery::DiscoveryApi;
use crate::entities::EntitiesApi;
use crate::error::{self, Error, Result};
use crate::market::MarketApi;
use crate::nft::NftApi;
use crate::resolve::ResolveApi;
use crate::token::TokenApi;
use crate::transaction::TransactionApi;
use crate::utils::UtilsApi;
use crate::volume::VolumeApi;
use crate::wallet::WalletApi;
use reqwest::Client as HttpClient;
use secrecy::{ExposeSecret, SecretString};
use serde::de::DeserializeOwned;
use std::time::Duration;
use yldfi_common::http::HttpClientConfig;
const BASE_URL: &str = "https://deep-index.moralis.io/api/v2.2";
#[derive(Clone)]
pub struct Config {
pub api_key: SecretString,
pub base_url: String,
pub http: HttpClientConfig,
}
impl Config {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: SecretString::from(api_key.into()),
base_url: BASE_URL.to_string(),
http: HttpClientConfig::default(),
}
}
#[must_use]
pub fn base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.http.timeout = timeout;
self
}
#[must_use]
pub fn with_timeout(self, timeout: Duration) -> Self {
self.timeout(timeout)
}
#[must_use]
pub fn proxy(mut self, proxy: impl Into<String>) -> Self {
self.http.proxy = Some(proxy.into());
self
}
#[must_use]
pub fn optional_proxy(mut self, proxy: Option<String>) -> Self {
self.http.proxy = proxy;
self
}
}
impl std::fmt::Debug for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Config")
.field("api_key", &"[REDACTED]")
.field("base_url", &self.base_url)
.field("http", &self.http)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct Client {
http: HttpClient,
api_key: SecretString,
base_url: String,
}
impl Client {
pub fn new(api_key: impl Into<String>) -> Result<Self> {
Self::with_config(Config::new(api_key))
}
pub fn from_env() -> Result<Self> {
let api_key = std::env::var("MORALIS_API_KEY").map_err(|_| error::missing_api_key())?;
Self::new(api_key)
}
pub fn with_config(config: Config) -> Result<Self> {
if config.api_key.expose_secret().is_empty() {
return Err(error::missing_api_key());
}
let is_localhost = if let Ok(parsed_url) = reqwest::Url::parse(&config.base_url) {
parsed_url
.host_str()
.is_some_and(|host| host == "localhost" || host == "127.0.0.1" || host == "::1")
} else {
false
};
if !config.base_url.starts_with("https://") && !is_localhost {
return Err(error::insecure_scheme(&config.base_url));
}
let http = yldfi_common::build_client(&config.http)?;
Ok(Self {
http,
api_key: config.api_key,
base_url: config.base_url,
})
}
fn get_retry_after(response: &reqwest::Response) -> Option<u64> {
response
.headers()
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse().ok())
}
fn join_url(&self, path: &str) -> String {
let base = self.base_url.trim_end_matches('/');
let path = path.trim_start_matches('/');
format!("{base}/{path}")
}
async fn handle_error_response(response: reqwest::Response) -> Error {
let status = response.status().as_u16();
let retry_after = Self::get_retry_after(&response);
let body = response.text().await.unwrap_or_default();
Error::from_response(status, &body, retry_after)
}
pub(crate) async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
let url = self.join_url(path);
let response = self
.http
.get(&url)
.header("X-API-Key", self.api_key.expose_secret())
.send()
.await?;
if !response.status().is_success() {
return Err(Self::handle_error_response(response).await);
}
let data = response.json().await?;
Ok(data)
}
pub(crate) async fn get_with_query<T: DeserializeOwned, Q: serde::Serialize>(
&self,
path: &str,
query: &Q,
) -> Result<T> {
let url = self.join_url(path);
let response = self
.http
.get(&url)
.header("X-API-Key", self.api_key.expose_secret())
.query(query)
.send()
.await?;
if !response.status().is_success() {
return Err(Self::handle_error_response(response).await);
}
let data = response.json().await?;
Ok(data)
}
pub(crate) async fn post<T: DeserializeOwned, B: serde::Serialize>(
&self,
path: &str,
body: &B,
) -> Result<T> {
let url = self.join_url(path);
let response = self
.http
.post(&url)
.header("X-API-Key", self.api_key.expose_secret())
.json(body)
.send()
.await?;
if !response.status().is_success() {
return Err(Self::handle_error_response(response).await);
}
let data = response.json().await?;
Ok(data)
}
pub(crate) async fn post_with_query<
T: DeserializeOwned,
B: serde::Serialize,
Q: serde::Serialize,
>(
&self,
path: &str,
body: &B,
query: &Q,
) -> Result<T> {
let url = self.join_url(path);
let response = self
.http
.post(&url)
.header("X-API-Key", self.api_key.expose_secret())
.query(query)
.json(body)
.send()
.await?;
if !response.status().is_success() {
return Err(Self::handle_error_response(response).await);
}
let data = response.json().await?;
Ok(data)
}
pub(crate) async fn put<T: DeserializeOwned, B: serde::Serialize>(
&self,
path: &str,
body: &B,
) -> Result<T> {
let url = self.join_url(path);
let response = self
.http
.put(&url)
.header("X-API-Key", self.api_key.expose_secret())
.json(body)
.send()
.await?;
if !response.status().is_success() {
return Err(Self::handle_error_response(response).await);
}
let data = response.json().await?;
Ok(data)
}
#[must_use]
pub fn wallet(&self) -> WalletApi<'_> {
WalletApi::new(self)
}
#[must_use]
pub fn token(&self) -> TokenApi<'_> {
TokenApi::new(self)
}
#[must_use]
pub fn nft(&self) -> NftApi<'_> {
NftApi::new(self)
}
#[must_use]
pub fn block(&self) -> BlockApi<'_> {
BlockApi::new(self)
}
#[must_use]
pub fn transaction(&self) -> TransactionApi<'_> {
TransactionApi::new(self)
}
#[must_use]
pub fn defi(&self) -> DefiApi<'_> {
DefiApi::new(self)
}
#[must_use]
pub fn resolve(&self) -> ResolveApi<'_> {
ResolveApi::new(self)
}
#[must_use]
pub fn market(&self) -> MarketApi<'_> {
MarketApi::new(self)
}
#[must_use]
pub fn discovery(&self) -> DiscoveryApi<'_> {
DiscoveryApi::new(self)
}
#[must_use]
pub fn entities(&self) -> EntitiesApi<'_> {
EntitiesApi::new(self)
}
#[must_use]
pub fn utils(&self) -> UtilsApi<'_> {
UtilsApi::new(self)
}
#[must_use]
pub fn volume(&self) -> VolumeApi<'_> {
VolumeApi::new(self)
}
#[must_use]
pub fn analytics(&self) -> AnalyticsApi<'_> {
AnalyticsApi::new(self)
}
}