polyte_clob/
request.rs

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/// Authentication mode for requests
16#[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
31/// Generic request builder for CLOB API
32pub 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    /// Create a new GET request
46    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    /// Create a new POST request
67    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    /// Create a new DELETE request
88    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    /// Set request body
109    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    /// Execute the request and deserialize response
123    pub async fn send(self) -> Result<T> {
124        let response = self.send_raw().await?;
125
126        // Get text for debugging
127        let text = response.text().await?;
128
129        tracing::debug!("Response body: {}", text);
130
131        // Deserialize and provide better error context
132        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    /// Execute the request and return raw response
140    pub async fn send_raw(self) -> Result<Response> {
141        let url = self.base_url.join(&self.path)?;
142
143        // Build the base request
144        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        // Add query parameters
164        if !self.query.is_empty() {
165            request = request.query(&self.query);
166        }
167
168        // Add authentication headers
169        request = self.add_auth_headers(request).await?;
170
171        tracing::debug!("Sending {} request to: {:?}", self.method, request);
172
173        // Execute request
174        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    /// Add authentication headers based on auth mode
189    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}