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