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