use crate::error::{self, Result};
use crate::types::{
AllowanceResponse, ApiErrorResponse, ApprovalTransaction, Chain, LiquiditySource,
LiquiditySourcesResponse, QuoteRequest, QuoteResponse, SpenderResponse, SwapRequest,
SwapResponse, TokenInfo, TokenListResponse,
};
use reqwest::Client as HttpClient;
use serde::de::DeserializeOwned;
use std::collections::HashMap;
use std::time::Duration;
use yldfi_common::http::HttpClientConfig;
pub const DEFAULT_BASE_URL: &str = "https://api.1inch.dev";
const SWAP_API_VERSION: &str = "v6.0";
#[derive(Debug, Clone)]
pub struct Config {
pub api_key: String,
pub base_url: String,
pub http: HttpClientConfig,
}
impl Config {
#[must_use]
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
base_url: DEFAULT_BASE_URL.to_string(),
http: HttpClientConfig::default(),
}
}
#[must_use]
pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.http.timeout = timeout;
self
}
#[must_use]
pub fn with_proxy(mut self, proxy: impl Into<String>) -> Self {
self.http.proxy = Some(proxy.into());
self
}
#[must_use]
pub fn with_optional_proxy(mut self, proxy: Option<String>) -> Self {
self.http.proxy = proxy;
self
}
#[must_use]
pub fn optional_proxy(self, proxy: Option<String>) -> Self {
self.with_optional_proxy(proxy)
}
}
#[derive(Debug, Clone)]
pub struct Client {
http: HttpClient,
config: Config,
}
impl Client {
pub fn new(api_key: impl Into<String>) -> Result<Self> {
Self::with_config(Config::new(api_key))
}
pub fn with_config(config: Config) -> Result<Self> {
let http = yldfi_common::build_client(&config.http)?;
Ok(Self { http, config })
}
#[must_use]
pub fn http(&self) -> &HttpClient {
&self.http
}
#[must_use]
pub fn config(&self) -> &Config {
&self.config
}
fn swap_url(&self, chain: Chain, endpoint: &str) -> String {
format!(
"{}/swap/{}/{}/{}",
self.config.base_url,
SWAP_API_VERSION,
chain.chain_id(),
endpoint
)
}
async fn get_with_params<T: DeserializeOwned>(
&self,
url: &str,
params: &[(&str, String)],
) -> Result<T> {
let response = self
.http
.get(url)
.header("Authorization", format!("Bearer {}", self.config.api_key))
.header("Accept", "application/json")
.query(params)
.send()
.await?;
let status = response.status().as_u16();
if !response.status().is_success() {
let body = response.text().await.unwrap_or_default();
if let Ok(error_response) = serde_json::from_str::<ApiErrorResponse>(&body) {
let message = error_response
.description
.or(error_response.error)
.unwrap_or_else(|| body.clone());
return Err(error::from_response(status, &message, None));
}
return Err(error::from_response(status, &body, None));
}
let data = response.json().await?;
Ok(data)
}
async fn get<T: DeserializeOwned>(&self, url: &str) -> Result<T> {
self.get_with_params(url, &[]).await
}
pub async fn get_quote(&self, chain: Chain, request: &QuoteRequest) -> Result<QuoteResponse> {
let url = self.swap_url(chain, "quote");
let params = request.to_query_params();
self.get_with_params(&url, ¶ms).await
}
pub async fn get_swap(&self, chain: Chain, request: &SwapRequest) -> Result<SwapResponse> {
let url = self.swap_url(chain, "swap");
let params = request.to_query_params();
self.get_with_params(&url, ¶ms).await
}
pub async fn get_tokens(&self, chain: Chain) -> Result<HashMap<String, TokenInfo>> {
let url = self.swap_url(chain, "tokens");
let response: TokenListResponse = self.get(&url).await?;
Ok(response.tokens)
}
pub async fn get_liquidity_sources(&self, chain: Chain) -> Result<Vec<LiquiditySource>> {
let url = self.swap_url(chain, "liquidity-sources");
let response: LiquiditySourcesResponse = self.get(&url).await?;
Ok(response.protocols)
}
pub async fn get_approve_spender(&self, chain: Chain) -> Result<String> {
let url = self.swap_url(chain, "approve/spender");
let response: SpenderResponse = self.get(&url).await?;
Ok(response.address)
}
pub async fn get_approve_allowance(
&self,
chain: Chain,
token_address: &str,
wallet_address: &str,
) -> Result<String> {
let url = self.swap_url(chain, "approve/allowance");
let params = vec![
("tokenAddress", token_address.to_string()),
("walletAddress", wallet_address.to_string()),
];
let response: AllowanceResponse = self.get_with_params(&url, ¶ms).await?;
Ok(response.allowance)
}
pub async fn get_approve_transaction(
&self,
chain: Chain,
token_address: &str,
amount: Option<&str>,
) -> Result<ApprovalTransaction> {
let url = self.swap_url(chain, "approve/transaction");
let mut params = vec![("tokenAddress", token_address.to_string())];
if let Some(amt) = amount {
params.push(("amount", amt.to_string()));
}
self.get_with_params(&url, ¶ms).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_builder() {
let config = Config::new("test-api-key")
.with_base_url("https://custom.api.com")
.with_timeout(Duration::from_secs(60));
assert_eq!(config.api_key, "test-api-key");
assert_eq!(config.base_url, "https://custom.api.com");
assert_eq!(config.http.timeout, Duration::from_secs(60));
}
#[test]
fn test_client_creation() {
let client = Client::new("test-api-key");
assert!(client.is_ok());
let client = client.unwrap();
assert_eq!(client.config().api_key, "test-api-key");
assert_eq!(client.config().base_url, DEFAULT_BASE_URL);
}
#[test]
fn test_swap_url_generation() {
let client = Client::new("test-api-key").unwrap();
let url = client.swap_url(Chain::Ethereum, "quote");
assert_eq!(url, "https://api.1inch.dev/swap/v6.0/1/quote");
let url = client.swap_url(Chain::Polygon, "swap");
assert_eq!(url, "https://api.1inch.dev/swap/v6.0/137/swap");
let url = client.swap_url(Chain::Arbitrum, "tokens");
assert_eq!(url, "https://api.1inch.dev/swap/v6.0/42161/tokens");
}
#[test]
fn test_custom_base_url() {
let config = Config::new("test-api-key").with_base_url("https://custom.1inch.io");
let client = Client::with_config(config).unwrap();
let url = client.swap_url(Chain::Ethereum, "quote");
assert_eq!(url, "https://custom.1inch.io/swap/v6.0/1/quote");
}
}