use reqwest::header::{HeaderMap, HeaderValue};
use reqwest::Client as HttpClient;
use reqwest::StatusCode;
use serde::{de::DeserializeOwned, Serialize};
use url::Url;
use uuid::Uuid;
use crate::{
types::{
intents::{AddApprovalRequest, CreateIntentRequest, CreateIntentResponse},
networks::Network,
pagination::PaginatedResponse,
quotes::{GetTheBestQuoteRequest, Quote},
swaps::{ListHistoryParams, SwapWithAdditionalInfo},
tokens::{ListTokensParams, Token},
},
ApiClientError, ErrorResponse,
};
const AUTH_HEADER: &str = "X-Api-Key";
#[derive(Clone)]
pub struct Client {
base_url: Url,
http: HttpClient,
}
impl Client {
pub fn new(api_key: String, base_url: String) -> Result<Client, ApiClientError> {
let mut base_url = Url::parse(&base_url)
.map_err(|e| ApiClientError::InvalidUrl(format!("Invalid base_url: {}", e)))?;
if !base_url.path().ends_with('/') {
let new_path = format!("{}/", base_url.path());
base_url.set_path(&new_path);
}
let mut headers = HeaderMap::with_capacity(1);
let header_value = HeaderValue::from_str(&api_key)
.map_err(|e| ApiClientError::InvalidApiKey(e.to_string()))?;
headers.insert(AUTH_HEADER, header_value);
let http = HttpClient::builder()
.default_headers(headers)
.build()
.map_err(|e| ApiClientError::ClientInitialization(e.to_string()))?;
Ok(Self { base_url, http })
}
pub async fn list_networks(&self) -> Result<Vec<Network>, ApiClientError> {
let url = self
.base_url
.join("v1/networks")
.map_err(|e| ApiClientError::InvalidUrl(e.to_string()))?;
self.get(url.as_str().to_owned()).await
}
pub async fn list_tokens(
&self,
params: ListTokensParams,
) -> Result<PaginatedResponse<Token>, ApiClientError> {
let mut url = self
.base_url
.join("v1/tokens")
.map_err(|e| ApiClientError::InvalidUrl(e.to_string()))?;
let query = serde_urlencoded::to_string(¶ms)
.map_err(|e| ApiClientError::SerializationError(e.to_string()))?;
url.set_query(Some(&query));
self.get(url.as_str().to_owned()).await
}
pub async fn get_quote(&self, req: GetTheBestQuoteRequest) -> Result<Quote, ApiClientError> {
let url = self
.base_url
.join("v1/quotes/best")
.map_err(|e| ApiClientError::InvalidUrl(e.to_string()))?;
self.post(url.as_str().to_owned(), &req).await
}
pub async fn create_intent(
&self,
req: CreateIntentRequest,
) -> Result<CreateIntentResponse, ApiClientError> {
let url = self
.base_url
.join("v1/intents")
.map_err(|e| ApiClientError::InvalidUrl(e.to_string()))?;
self.post(url.as_str().to_owned(), &req).await
}
pub async fn add_approval_to_intent(
&self,
intent_id: Uuid,
req: AddApprovalRequest,
) -> Result<(), ApiClientError> {
let path = format!("v1/intents/{intent_id}/approvals");
let url = self
.base_url
.join(&path)
.map_err(|e| ApiClientError::InvalidUrl(e.to_string()))?;
self.post(url.as_str().to_owned(), &req).await
}
pub async fn get_swap_by_intent_id(
&self,
intent_id: Uuid,
) -> Result<SwapWithAdditionalInfo, ApiClientError> {
let path = format!("v1/intents/{intent_id}");
let url = self
.base_url
.join(&path)
.map_err(|e| ApiClientError::InvalidUrl(e.to_string()))?;
self.get(url.as_str().to_owned()).await
}
pub async fn list_swap_history(
&self,
params: ListHistoryParams,
) -> Result<PaginatedResponse<SwapWithAdditionalInfo>, ApiClientError> {
let mut url = self
.base_url
.join("v1/swaps/history")
.map_err(|e| ApiClientError::InvalidUrl(e.to_string()))?;
let mut query = serde_urlencoded::to_string(¶ms)
.map_err(|e| ApiClientError::SerializationError(e.to_string()))?;
for wallet in ¶ms.wallets {
if !query.is_empty() {
query.push('&');
}
query.push_str(&format!("wallet={}", urlencoding::encode(wallet)));
}
url.set_query(Some(&query));
self.get(url.as_str().to_owned()).await
}
async fn get<RS>(&self, url: String) -> Result<RS, ApiClientError>
where
RS: DeserializeOwned,
{
let response = self.http.get(&url).send().await?;
let status = response.status();
if status.is_success() {
let result = response
.json::<RS>()
.await
.map_err(|e| ApiClientError::DeserializationError(e.to_string()))?;
return Ok(result);
}
if status == StatusCode::NOT_FOUND {
return Err(ApiClientError::NotFound);
}
if status.is_client_error() || status.is_server_error() {
let body = response
.text()
.await
.map_err(|e| ApiClientError::Network(e.to_string()))?;
if let Ok(error_response) = serde_json::from_str::<ErrorResponse>(&body) {
return Err(ApiClientError::ServerErrorResponse(error_response));
}
return Err(ApiClientError::DeserializationError(format!(
"Failed to parse error response: {}",
body
)));
}
Err(ApiClientError::Network(format!(
"Unexpected status code: {}",
status
)))
}
async fn post<RQ, RS>(&self, url: String, request: &RQ) -> Result<RS, ApiClientError>
where
RQ: Serialize + ?Sized,
RS: DeserializeOwned,
{
let response = self.http.post(&url).json(request).send().await?;
let status = response.status();
if status.is_success() {
let result = response
.json::<RS>()
.await
.map_err(|e| ApiClientError::DeserializationError(e.to_string()))?;
return Ok(result);
}
if status == StatusCode::NOT_FOUND {
return Err(ApiClientError::NotFound);
}
if status.is_client_error() || status.is_server_error() {
let body = response
.text()
.await
.map_err(|e| ApiClientError::Network(e.to_string()))?;
if let Ok(error_response) = serde_json::from_str::<ErrorResponse>(&body) {
return Err(ApiClientError::ServerErrorResponse(error_response));
}
return Err(ApiClientError::DeserializationError(format!(
"Failed to parse error response: {}",
body
)));
}
Err(ApiClientError::Network(format!(
"Unexpected status code: {}",
status
)))
}
}