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?;
126
127 serde_json::from_str(&text).map_err(|e| {
129 tracing::error!("Deserialization failed: {}", e);
130 tracing::error!("Failed to deserialize: {}", text);
131 e.into()
132 })
133 }
134
135 pub async fn send_raw(self) -> Result<Response, ClobError> {
137 let url = self.base_url.join(&self.path)?;
138
139 let mut request = match self.method {
141 Method::GET => self.client.get(url),
142 Method::POST => {
143 let mut req = self.client.post(url);
144 if let Some(body) = &self.body {
145 req = req.header("Content-Type", "application/json").json(body);
146 }
147 req
148 }
149 Method::DELETE => {
150 let mut req = self.client.delete(url);
151 if let Some(body) = &self.body {
152 req = req.header("Content-Type", "application/json").json(body);
153 }
154 req
155 }
156 _ => return Err(ClobError::validation("Unsupported HTTP method")),
157 };
158
159 if !self.query.is_empty() {
161 request = request.query(&self.query);
162 }
163
164 request = self.add_auth_headers(request).await?;
166
167 let response = request.send().await?;
169 let status = response.status();
170
171 tracing::debug!("Response status: {}", status);
172
173 if !status.is_success() {
174 let error = ClobError::from_response(response).await;
175 tracing::error!("Request failed: {:?}", error);
176 return Err(error);
177 }
178
179 Ok(response)
180 }
181
182 async fn add_auth_headers(
184 &self,
185 mut request: reqwest::RequestBuilder,
186 ) -> Result<reqwest::RequestBuilder, ClobError> {
187 match &self.auth {
188 AuthMode::None => Ok(request),
189 AuthMode::L1 {
190 wallet,
191 nonce,
192 timestamp,
193 } => {
194 use crate::core::eip712::sign_clob_auth;
195
196 let signature =
197 sign_clob_auth(wallet.signer(), self.chain_id, *timestamp, *nonce).await?;
198
199 request = request
200 .header("POLY_ADDRESS", format!("{:?}", wallet.address()))
201 .header("POLY_SIGNATURE", signature)
202 .header("POLY_TIMESTAMP", timestamp.to_string())
203 .header("POLY_NONCE", nonce.to_string());
204
205 Ok(request)
206 }
207 AuthMode::L2 {
208 address,
209 credentials,
210 signer,
211 } => {
212 let timestamp = current_timestamp();
213 let body_str = self.body.as_ref().map(|b| b.to_string());
214 let message = Signer::create_message(
215 timestamp,
216 self.method.as_str(),
217 &self.path,
218 body_str.as_deref(),
219 );
220 let signature = signer.sign(&message)?;
221
222 request = request
223 .header("POLY_ADDRESS", format!("{:?}", address))
224 .header("POLY_SIGNATURE", signature)
225 .header("POLY_TIMESTAMP", timestamp.to_string())
226 .header("POLY_API_KEY", &credentials.key)
227 .header("POLY_PASSPHRASE", &credentials.passphrase);
228
229 Ok(request)
230 }
231 }
232 }
233}