Skip to main content

polyoxide_clob/
request.rs

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