use reqwest::{Client as ReqwestClient, header};
use serde::{de::DeserializeOwned};
use serde_json::Value;
use std::collections::HashMap;
use crate::error::{Result, APIError, InvalidInputsError};
use crate::calldata_queue::CalldataQueue;
use crate::utils::checksum_addresses_in_json;
use crate::config::{ADDRESS_BOOK_ENDPOINT, USER_AGENT};
use crate::Arc;
use crate::config::DEFAULT_BASE_URL;
#[derive(Debug, Clone)]
pub struct Client {
pub http_client: ReqwestClient,
pub api_key: String,
pub base_url: String,
pub address_book: HashMap<String, Value>,
}
impl Client {
pub async fn new(api_key: String, base_url: Option<String>) -> Result<Self> {
let base_url = base_url.unwrap_or_else(||
DEFAULT_BASE_URL.to_string()
);
let mut headers = header::HeaderMap::new();
headers.insert(
"x-api-key",
header::HeaderValue::from_str(&api_key)
.expect("Invalid API key format"),
);
headers.insert(
"Content-Type",
header::HeaderValue::from_static("application/json"),
);
headers.insert(
"User-Agent",
header::HeaderValue::from_static(USER_AGENT),
);
let http_client = ReqwestClient::builder()
.default_headers(headers)
.build()
.expect("Failed to create HTTP client");
let address_book_response = ReqwestClient::new()
.get(ADDRESS_BOOK_ENDPOINT)
.send()
.await
.map_err(|e| APIError::new(e.to_string(), 500))?;
let address_book: HashMap<String, Value> = address_book_response
.json()
.await
.map_err(|e| APIError::new(e.to_string(), 500))?;
let address_book = checksum_addresses_in_json(address_book)?;
Ok(Self {
http_client,
api_key,
base_url,
address_book,
})
}
async fn request<T: DeserializeOwned>(
&self,
method: reqwest::Method,
endpoint: &str,
params: Option<&HashMap<String, String>>,
json_data: Option<&Value>,
) -> Result<T> {
let url = format!("{}{}", self.base_url, endpoint);
let mut request = self.http_client
.request(method, &url);
if let Some(params) = params {
request = request.query(params);
}
if let Some(data) = json_data {
request = request.json(data);
}
let response = request
.send()
.await
.map_err(|e| APIError::new(e.to_string(), 500))?;
let status = response.status();
if !status.is_success() {
let error_text = response.text().await
.unwrap_or_else(|_| "No error message".to_string());
if let Ok(error_json) = serde_json::from_str::<Value>(&error_text) {
if let Some(message) = error_json.get("message")
.and_then(|m| m.as_str())
.filter(|m| !m.is_empty())
{
return Err(APIError::new(message.to_string(), status.as_u16() as i32).into());
}
}
return Err(APIError::new(error_text, status.as_u16() as i32).into());
}
response.json::<T>()
.await
.map_err(|e| APIError::new(e.to_string(), 500).into())
}
pub async fn create_calldata_queue(
&self,
chain_id: u64,
strategist_address: String,
rpc_url: String,
symbol: String,
) -> Result<CalldataQueue> {
CalldataQueue::new(
chain_id,
strategist_address,
rpc_url,
symbol,
Arc::new(self.clone())
).await
}
pub async fn get<T: DeserializeOwned>(
&self,
endpoint: &str,
params: Option<&HashMap<String, String>>,
) -> Result<T> {
self.request(reqwest::Method::GET, endpoint, params, None).await
}
pub async fn post<T: DeserializeOwned>(
&self,
endpoint: &str,
data: Option<&Value>,
) -> Result<T> {
self.request(reqwest::Method::POST, endpoint, None, data).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use crate::calldata_queue::CalldataQueue;
#[tokio::test]
async fn test_address_book() {
let client = Client::new(
"test_api_key".to_string(),
None
).await.expect("Failed to create client");
assert!(client.address_book.contains_key("1"));
}
#[tokio::test]
async fn test_post_unknown_endpoint() {
let client = Client::new(
"test_api_key".to_string(),
None
).await.expect("Failed to create client");
let result: Result<serde_json::Value> = client.post("unknown_endpoint", None).await;
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("API Error (403): Missing Authentication Token"));
}
}