1use std::marker::PhantomData;
2
3use alloy::primitives::Address;
4use polyoxide_core::{current_timestamp, request::QueryBuilder, retry_after_header, HttpClient};
5use reqwest::{Method, Response};
6use serde::de::DeserializeOwned;
7
8use crate::{
9 account::{Credentials, Signer, Wallet},
10 error::ClobError,
11};
12
13#[derive(Debug, Clone)]
15pub enum AuthMode {
16 None,
17 L1 {
18 wallet: Wallet,
19 nonce: u32,
20 },
21 L2 {
22 address: Address,
23 credentials: Credentials,
24 signer: Signer,
25 },
26}
27
28pub struct Request<T> {
30 pub(crate) http_client: HttpClient,
31 pub(crate) path: String,
32 pub(crate) method: Method,
33 pub(crate) query: Vec<(String, String)>,
34 pub(crate) body: Option<serde_json::Value>,
35 pub(crate) auth: AuthMode,
36 pub(crate) chain_id: u64,
37 pub(crate) _marker: PhantomData<T>,
38}
39
40impl<T> Request<T> {
41 pub(crate) fn get(
43 http_client: HttpClient,
44 path: impl Into<String>,
45 auth: AuthMode,
46 chain_id: u64,
47 ) -> Self {
48 Self {
49 http_client,
50 path: path.into(),
51 method: Method::GET,
52 query: Vec::new(),
53 body: None,
54 auth,
55 chain_id,
56 _marker: PhantomData,
57 }
58 }
59
60 pub(crate) fn post(
62 http_client: HttpClient,
63 path: String,
64 auth: AuthMode,
65 chain_id: u64,
66 ) -> Self {
67 Self {
68 http_client,
69 path,
70 method: Method::POST,
71 query: Vec::new(),
72 body: None,
73 auth,
74 chain_id,
75 _marker: PhantomData,
76 }
77 }
78
79 pub(crate) fn delete(
81 http_client: HttpClient,
82 path: impl Into<String>,
83 auth: AuthMode,
84 chain_id: u64,
85 ) -> Self {
86 Self {
87 http_client,
88 path: path.into(),
89 method: Method::DELETE,
90 query: Vec::new(),
91 body: None,
92 auth,
93 chain_id,
94 _marker: PhantomData,
95 }
96 }
97
98 pub fn body<B: serde::Serialize + ?Sized>(mut self, body: &B) -> Result<Self, ClobError> {
100 self.body = Some(serde_json::to_value(body)?);
101 Ok(self)
102 }
103}
104
105impl<T> QueryBuilder for Request<T> {
106 fn add_query(&mut self, key: String, value: String) {
107 self.query.push((key, value));
108 }
109}
110
111impl<T: DeserializeOwned> Request<T> {
112 pub async fn send(self) -> Result<T, ClobError> {
114 let response = self.send_raw().await?;
115
116 let text = response.text().await?;
117
118 serde_json::from_str(&text).map_err(|e| {
120 tracing::error!("Deserialization failed: {}", e);
121 tracing::error!(
122 "Failed to deserialize: {}",
123 polyoxide_core::truncate_for_log(&text)
124 );
125 e.into()
126 })
127 }
128
129 pub async fn send_raw(self) -> Result<Response, ClobError> {
131 let url = self.http_client.base_url.join(&self.path)?;
132
133 let http_client = self.http_client;
134 let path = self.path;
135 let method = self.method;
136 let query = self.query;
137 let body = self.body;
138 let auth = self.auth;
139 let chain_id = self.chain_id;
140 let mut attempt = 0u32;
141
142 loop {
143 http_client.acquire_rate_limit(&path, Some(&method)).await;
144
145 let mut request = match method {
147 Method::GET => http_client.client.get(url.clone()),
148 Method::POST => {
149 let mut req = http_client.client.post(url.clone());
150 if let Some(ref body) = body {
151 req = req.header("Content-Type", "application/json").json(body);
152 }
153 req
154 }
155 Method::DELETE => {
156 let mut req = http_client.client.delete(url.clone());
157 if let Some(ref body) = body {
158 req = req.header("Content-Type", "application/json").json(body);
159 }
160 req
161 }
162 _ => return Err(ClobError::validation("Unsupported HTTP method")),
163 };
164
165 if !query.is_empty() {
167 request = request.query(&query);
168 }
169
170 request = add_auth_headers(request, &auth, &path, &method, &body, chain_id).await?;
172
173 let response = request.send().await?;
175 let status = response.status();
176 let retry_after = retry_after_header(&response);
177
178 if let Some(backoff) = http_client.should_retry(status, attempt, retry_after.as_deref())
179 {
180 attempt += 1;
181 tracing::warn!(
182 "Rate limited (429) on {}, retry {} after {}ms",
183 path,
184 attempt,
185 backoff.as_millis()
186 );
187 tokio::time::sleep(backoff).await;
188 continue;
189 }
190
191 tracing::debug!("Response status: {}", status);
192
193 if !status.is_success() {
194 let error = ClobError::from_response(response).await;
195 tracing::error!("Request failed: {:?}", error);
196 return Err(error);
197 }
198
199 return Ok(response);
200 }
201 }
202}
203
204async fn add_auth_headers(
206 mut request: reqwest::RequestBuilder,
207 auth: &AuthMode,
208 path: &str,
209 method: &Method,
210 body: &Option<serde_json::Value>,
211 chain_id: u64,
212) -> Result<reqwest::RequestBuilder, ClobError> {
213 match auth {
214 AuthMode::None => Ok(request),
215 AuthMode::L1 { wallet, nonce } => {
216 use crate::core::eip712::sign_clob_auth;
217
218 let timestamp = current_timestamp();
220 let signature = sign_clob_auth(wallet.signer(), chain_id, timestamp, *nonce).await?;
221
222 request = request
223 .header("POLY_ADDRESS", format!("{:?}", wallet.address()))
224 .header("POLY_SIGNATURE", signature)
225 .header("POLY_TIMESTAMP", timestamp.to_string())
226 .header("POLY_NONCE", nonce.to_string());
227
228 Ok(request)
229 }
230 AuthMode::L2 {
231 address,
232 credentials,
233 signer,
234 } => {
235 let timestamp = current_timestamp();
236 let body_str = body.as_ref().map(|b| b.to_string());
237 let message =
238 Signer::create_message(timestamp, method.as_str(), path, body_str.as_deref());
239 let signature = signer.sign(&message)?;
240
241 request = request
242 .header("POLY_ADDRESS", format!("{:?}", address))
243 .header("POLY_SIGNATURE", signature)
244 .header("POLY_TIMESTAMP", timestamp.to_string())
245 .header("POLY_API_KEY", &credentials.key)
246 .header("POLY_PASSPHRASE", &credentials.passphrase);
247
248 Ok(request)
249 }
250 }
251}