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