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