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(crate) fn put(
100 http_client: HttpClient,
101 path: impl Into<String>,
102 auth: AuthMode,
103 chain_id: u64,
104 ) -> Self {
105 Self {
106 http_client,
107 path: path.into(),
108 method: Method::PUT,
109 query: Vec::new(),
110 body: None,
111 auth,
112 chain_id,
113 _marker: PhantomData,
114 }
115 }
116
117 pub fn body<B: serde::Serialize + ?Sized>(mut self, body: &B) -> Result<Self, ClobError> {
119 self.body = Some(serde_json::to_value(body)?);
120 Ok(self)
121 }
122}
123
124impl<T> QueryBuilder for Request<T> {
125 fn add_query(&mut self, key: String, value: String) {
126 self.query.push((key, value));
127 }
128}
129
130impl<T: DeserializeOwned> Request<T> {
131 pub async fn send(self) -> Result<T, ClobError> {
133 let response = self.send_raw().await?;
134
135 let text = response.text().await?;
136
137 serde_json::from_str(&text).map_err(|e| {
139 tracing::error!("Deserialization failed: {}", e);
140 tracing::error!(
141 "Failed to deserialize: {}",
142 polyoxide_core::truncate_for_log(&text)
143 );
144 e.into()
145 })
146 }
147
148 pub async fn send_raw(self) -> Result<Response, ClobError> {
150 let url = self.http_client.base_url.join(&self.path)?;
151
152 let http_client = self.http_client;
153 let path = self.path;
154 let method = self.method;
155 let query = self.query;
156 let body = self.body;
157 let auth = self.auth;
158 let chain_id = self.chain_id;
159 let mut attempt = 0u32;
160
161 loop {
162 let _permit = http_client.acquire_concurrency().await;
163 http_client.acquire_rate_limit(&path, Some(&method)).await;
164
165 let mut request = match method {
167 Method::GET => http_client.client.get(url.clone()),
168 Method::POST => {
169 let mut req = http_client.client.post(url.clone());
170 if let Some(ref body) = body {
171 req = req.header("Content-Type", "application/json").json(body);
172 }
173 req
174 }
175 Method::DELETE => {
176 let mut req = http_client.client.delete(url.clone());
177 if let Some(ref body) = body {
178 req = req.header("Content-Type", "application/json").json(body);
179 }
180 req
181 }
182 Method::PUT => {
183 let mut req = http_client.client.put(url.clone());
184 if let Some(ref body) = body {
185 req = req.header("Content-Type", "application/json").json(body);
186 }
187 req
188 }
189 _ => return Err(ClobError::validation("Unsupported HTTP method")),
190 };
191
192 if !query.is_empty() {
194 request = request.query(&query);
195 }
196
197 request = add_auth_headers(request, &auth, &path, &method, &body, chain_id).await?;
199
200 let response = request.send().await?;
202 let status = response.status();
203 let retry_after = retry_after_header(&response);
204
205 if let Some(backoff) = http_client.should_retry(status, attempt, retry_after.as_deref())
206 {
207 attempt += 1;
208 tracing::warn!(
209 "Rate limited (429) on {}, retry {} after {}ms",
210 path,
211 attempt,
212 backoff.as_millis()
213 );
214 drop(_permit);
215 tokio::time::sleep(backoff).await;
216 continue;
217 }
218
219 tracing::debug!("Response status: {}", status);
220
221 if !status.is_success() {
222 let error = ClobError::from_response(response).await;
223 tracing::error!("Request failed: {:?}", error);
224 return Err(error);
225 }
226
227 return Ok(response);
228 }
229 }
230}
231
232async fn add_auth_headers(
234 mut request: reqwest::RequestBuilder,
235 auth: &AuthMode,
236 path: &str,
237 method: &Method,
238 body: &Option<serde_json::Value>,
239 chain_id: u64,
240) -> Result<reqwest::RequestBuilder, ClobError> {
241 match auth {
242 AuthMode::None => Ok(request),
243 AuthMode::L1 { wallet, nonce } => {
244 use crate::core::eip712::sign_clob_auth;
245
246 let timestamp = current_timestamp();
248 let signature = sign_clob_auth(wallet.signer(), chain_id, timestamp, *nonce).await?;
249
250 request = request
251 .header("POLY_ADDRESS", format!("{:?}", wallet.address()))
252 .header("POLY_SIGNATURE", signature)
253 .header("POLY_TIMESTAMP", timestamp.to_string())
254 .header("POLY_NONCE", nonce.to_string());
255
256 Ok(request)
257 }
258 AuthMode::L2 {
259 address,
260 credentials,
261 signer,
262 } => {
263 let timestamp = current_timestamp();
264 let body_str = body.as_ref().map(|b| b.to_string());
265 let message =
266 Signer::create_message(timestamp, method.as_str(), path, body_str.as_deref());
267 let signature = signer.sign(&message)?;
268
269 request = request
270 .header("POLY_ADDRESS", format!("{:?}", address))
271 .header("POLY_SIGNATURE", signature)
272 .header("POLY_TIMESTAMP", timestamp.to_string())
273 .header("POLY_API_KEY", &credentials.key)
274 .header("POLY_PASSPHRASE", &credentials.passphrase);
275
276 Ok(request)
277 }
278 }
279}