polyte_clob/
request.rs

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/// Authentication mode for requests
19#[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
34/// Generic request builder for CLOB API
35pub 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    /// Create a new GET request
49    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    /// Create a new POST request
70    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    /// Create a new DELETE request
91    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    /// Set request body
112    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    /// Execute the request and deserialize response
126    pub async fn send(self) -> Result<T> {
127        let response = self.send_raw().await?;
128
129        // Get text for debugging
130        let text = response.text().await?;
131
132        tracing::debug!("Response body: {}", text);
133
134        // Deserialize and provide better error context
135        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    /// Execute the request and return raw response
143    pub async fn send_raw(self) -> Result<Response> {
144        let url = self.base_url.join(&self.path)?;
145
146        // Build the base request
147        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        // Add query parameters
167        if !self.query.is_empty() {
168            request = request.query(&self.query);
169        }
170
171        // Add authentication headers
172        request = self.add_auth_headers(request).await?;
173
174        tracing::debug!("Sending {} request to: {:?}", self.method, request);
175
176        // Execute request
177        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    /// Add authentication headers based on auth mode
192    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}