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 let _permit = http_client.acquire_concurrency().await;
144 http_client.acquire_rate_limit(&path, Some(&method)).await;
145
146 let mut request = match method {
148 Method::GET => http_client.client.get(url.clone()),
149 Method::POST => {
150 let mut req = http_client.client.post(url.clone());
151 if let Some(ref body) = body {
152 req = req.header("Content-Type", "application/json").json(body);
153 }
154 req
155 }
156 Method::DELETE => {
157 let mut req = http_client.client.delete(url.clone());
158 if let Some(ref body) = body {
159 req = req.header("Content-Type", "application/json").json(body);
160 }
161 req
162 }
163 _ => return Err(ClobError::validation("Unsupported HTTP method")),
164 };
165
166 if !query.is_empty() {
168 request = request.query(&query);
169 }
170
171 request = add_auth_headers(request, &auth, &path, &method, &body, chain_id).await?;
173
174 let response = request.send().await?;
176 let status = response.status();
177 let retry_after = retry_after_header(&response);
178
179 if let Some(backoff) = http_client.should_retry(status, attempt, retry_after.as_deref())
180 {
181 attempt += 1;
182 tracing::warn!(
183 "Rate limited (429) on {}, retry {} after {}ms",
184 path,
185 attempt,
186 backoff.as_millis()
187 );
188 drop(_permit);
189 tokio::time::sleep(backoff).await;
190 continue;
191 }
192
193 tracing::debug!("Response status: {}", status);
194
195 if !status.is_success() {
196 let error = ClobError::from_response(response).await;
197 tracing::error!("Request failed: {:?}", error);
198 return Err(error);
199 }
200
201 return Ok(response);
202 }
203 }
204}
205
206async fn add_auth_headers(
208 mut request: reqwest::RequestBuilder,
209 auth: &AuthMode,
210 path: &str,
211 method: &Method,
212 body: &Option<serde_json::Value>,
213 chain_id: u64,
214) -> Result<reqwest::RequestBuilder, ClobError> {
215 match auth {
216 AuthMode::None => Ok(request),
217 AuthMode::L1 { wallet, nonce } => {
218 use crate::core::eip712::sign_clob_auth;
219
220 let timestamp = current_timestamp();
222 let signature = sign_clob_auth(wallet.signer(), chain_id, timestamp, *nonce).await?;
223
224 request = request
225 .header("POLY_ADDRESS", format!("{:?}", wallet.address()))
226 .header("POLY_SIGNATURE", signature)
227 .header("POLY_TIMESTAMP", timestamp.to_string())
228 .header("POLY_NONCE", nonce.to_string());
229
230 Ok(request)
231 }
232 AuthMode::L2 {
233 address,
234 credentials,
235 signer,
236 } => {
237 let timestamp = current_timestamp();
238 let body_str = body.as_ref().map(|b| b.to_string());
239 let message =
240 Signer::create_message(timestamp, method.as_str(), path, body_str.as_deref());
241 let signature = signer.sign(&message)?;
242
243 request = request
244 .header("POLY_ADDRESS", format!("{:?}", address))
245 .header("POLY_SIGNATURE", signature)
246 .header("POLY_TIMESTAMP", timestamp.to_string())
247 .header("POLY_API_KEY", &credentials.key)
248 .header("POLY_PASSPHRASE", &credentials.passphrase);
249
250 Ok(request)
251 }
252 }
253}