use std::marker::PhantomData;
use alloy::primitives::Address;
use polyoxide_core::{current_timestamp, request::QueryBuilder, retry_after_header, HttpClient};
use reqwest::{Method, Response};
use serde::de::DeserializeOwned;
use crate::{
account::{Credentials, Signer, Wallet},
error::ClobError,
};
#[derive(Debug, Clone)]
pub enum AuthMode {
None,
L1 {
wallet: Wallet,
nonce: u32,
},
L2 {
address: Address,
credentials: Credentials,
signer: Signer,
},
}
pub struct Request<T> {
pub(crate) http_client: HttpClient,
pub(crate) path: String,
pub(crate) method: Method,
pub(crate) query: Vec<(String, String)>,
pub(crate) body: Option<serde_json::Value>,
pub(crate) auth: AuthMode,
pub(crate) chain_id: u64,
pub(crate) _marker: PhantomData<T>,
}
impl<T> Request<T> {
pub(crate) fn get(
http_client: HttpClient,
path: impl Into<String>,
auth: AuthMode,
chain_id: u64,
) -> Self {
Self {
http_client,
path: path.into(),
method: Method::GET,
query: Vec::new(),
body: None,
auth,
chain_id,
_marker: PhantomData,
}
}
pub(crate) fn post(
http_client: HttpClient,
path: String,
auth: AuthMode,
chain_id: u64,
) -> Self {
Self {
http_client,
path,
method: Method::POST,
query: Vec::new(),
body: None,
auth,
chain_id,
_marker: PhantomData,
}
}
pub(crate) fn delete(
http_client: HttpClient,
path: impl Into<String>,
auth: AuthMode,
chain_id: u64,
) -> Self {
Self {
http_client,
path: path.into(),
method: Method::DELETE,
query: Vec::new(),
body: None,
auth,
chain_id,
_marker: PhantomData,
}
}
pub fn body<B: serde::Serialize + ?Sized>(mut self, body: &B) -> Result<Self, ClobError> {
self.body = Some(serde_json::to_value(body)?);
Ok(self)
}
}
impl<T> QueryBuilder for Request<T> {
fn add_query(&mut self, key: String, value: String) {
self.query.push((key, value));
}
}
impl<T: DeserializeOwned> Request<T> {
pub async fn send(self) -> Result<T, ClobError> {
let response = self.send_raw().await?;
let text = response.text().await?;
serde_json::from_str(&text).map_err(|e| {
tracing::error!("Deserialization failed: {}", e);
tracing::error!(
"Failed to deserialize: {}",
polyoxide_core::truncate_for_log(&text)
);
e.into()
})
}
pub async fn send_raw(self) -> Result<Response, ClobError> {
let url = self.http_client.base_url.join(&self.path)?;
let http_client = self.http_client;
let path = self.path;
let method = self.method;
let query = self.query;
let body = self.body;
let auth = self.auth;
let chain_id = self.chain_id;
let mut attempt = 0u32;
loop {
let _permit = http_client.acquire_concurrency().await;
http_client.acquire_rate_limit(&path, Some(&method)).await;
let mut request = match method {
Method::GET => http_client.client.get(url.clone()),
Method::POST => {
let mut req = http_client.client.post(url.clone());
if let Some(ref body) = body {
req = req.header("Content-Type", "application/json").json(body);
}
req
}
Method::DELETE => {
let mut req = http_client.client.delete(url.clone());
if let Some(ref body) = body {
req = req.header("Content-Type", "application/json").json(body);
}
req
}
_ => return Err(ClobError::validation("Unsupported HTTP method")),
};
if !query.is_empty() {
request = request.query(&query);
}
request = add_auth_headers(request, &auth, &path, &method, &body, chain_id).await?;
let response = request.send().await?;
let status = response.status();
let retry_after = retry_after_header(&response);
if let Some(backoff) = http_client.should_retry(status, attempt, retry_after.as_deref())
{
attempt += 1;
tracing::warn!(
"Rate limited (429) on {}, retry {} after {}ms",
path,
attempt,
backoff.as_millis()
);
drop(_permit);
tokio::time::sleep(backoff).await;
continue;
}
tracing::debug!("Response status: {}", status);
if !status.is_success() {
let error = ClobError::from_response(response).await;
tracing::error!("Request failed: {:?}", error);
return Err(error);
}
return Ok(response);
}
}
}
async fn add_auth_headers(
mut request: reqwest::RequestBuilder,
auth: &AuthMode,
path: &str,
method: &Method,
body: &Option<serde_json::Value>,
chain_id: u64,
) -> Result<reqwest::RequestBuilder, ClobError> {
match auth {
AuthMode::None => Ok(request),
AuthMode::L1 { wallet, nonce } => {
use crate::core::eip712::sign_clob_auth;
let timestamp = current_timestamp();
let signature = sign_clob_auth(wallet.signer(), chain_id, timestamp, *nonce).await?;
request = request
.header("POLY_ADDRESS", format!("{:?}", wallet.address()))
.header("POLY_SIGNATURE", signature)
.header("POLY_TIMESTAMP", timestamp.to_string())
.header("POLY_NONCE", nonce.to_string());
Ok(request)
}
AuthMode::L2 {
address,
credentials,
signer,
} => {
let timestamp = current_timestamp();
let body_str = body.as_ref().map(|b| b.to_string());
let message =
Signer::create_message(timestamp, method.as_str(), path, body_str.as_deref());
let signature = signer.sign(&message)?;
request = request
.header("POLY_ADDRESS", format!("{:?}", address))
.header("POLY_SIGNATURE", signature)
.header("POLY_TIMESTAMP", timestamp.to_string())
.header("POLY_API_KEY", &credentials.key)
.header("POLY_PASSPHRASE", &credentials.passphrase);
Ok(request)
}
}
}