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        let text = response.text().await?;
126
127        // Deserialize and provide better error context
128        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    /// Execute the request and return raw response
136    pub async fn send_raw(self) -> Result<Response, ClobError> {
137        let url = self.base_url.join(&self.path)?;
138
139        // Build the base request
140        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        // Add query parameters
160        if !self.query.is_empty() {
161            request = request.query(&self.query);
162        }
163
164        // Add authentication headers
165        request = self.add_auth_headers(request).await?;
166
167        // Execute request
168        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    /// Add authentication headers based on auth mode
183    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}