polymarket_rs/client/
authenticated.rs

1use crate::error::{Error, Result};
2use crate::http::{create_l1_headers, create_l2_headers, HttpClient};
3use crate::signing::EthSigner;
4use crate::types::{ApiCreds, ApiKeysResponse, BalanceAllowanceParams};
5use alloy_primitives::{Address, U256};
6
7/// Client for authenticated operations
8///
9/// This client handles operations that require authentication,
10/// such as API key management and account queries.
11///
12/// For PolyProxy wallets, the signer is used for API authentication
13/// while the funder address is used as the order maker.
14pub struct AuthenticatedClient {
15    http_client: HttpClient,
16    signer: Box<dyn EthSigner>,
17    chain_id: u64,
18    api_creds: Option<ApiCreds>,
19    funder: Option<Address>,
20}
21
22impl AuthenticatedClient {
23    /// Create a new AuthenticatedClient
24    ///
25    /// # Arguments
26    /// * `host` - The base URL for the API
27    /// * `signer` - The Ethereum signer (used for API authentication)
28    /// * `chain_id` - The chain ID (137 for Polygon, 80002 for Amoy testnet)
29    /// * `api_creds` - Optional API credentials for L2 operations
30    /// * `funder` - Optional funder address (for PolyProxy wallets, this is the proxy wallet address)
31    ///
32    /// # PolyProxy Wallets
33    /// For PolyProxy wallets:
34    /// - `signer`: Your EOA private key (delegated signer)
35    /// - `funder`: Your proxy wallet address (holds the funds)
36    /// - API authentication uses the signer address
37    /// - Orders are made by the funder address
38    pub fn new(
39        host: impl Into<String>,
40        signer: impl EthSigner + 'static,
41        chain_id: u64,
42        api_creds: Option<ApiCreds>,
43        funder: Option<Address>,
44    ) -> Self {
45        Self {
46            http_client: HttpClient::new(host),
47            signer: Box::new(signer),
48            chain_id,
49            api_creds,
50            funder,
51        }
52    }
53
54    /// Get the API credentials if available
55    ///
56    /// Returns a reference to the API credentials if they were provided when creating
57    /// the client. This is useful for accessing credentials for WebSocket authentication.
58    ///
59    /// # Example
60    ///
61    /// ```no_run
62    /// # use polymarket_rs::{AuthenticatedClient, ApiCreds};
63    /// # use polymarket_rs::websocket::UserWsClient;
64    /// # use alloy_signer_local::PrivateKeySigner;
65    /// # use futures_util::StreamExt;
66    /// # #[tokio::main]
67    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
68    /// # let signer = PrivateKeySigner::random();
69    /// # let creds = ApiCreds::new("key".into(), "secret".into(), "pass".into());
70    /// let auth_client = AuthenticatedClient::new(
71    ///     "https://clob.polymarket.com",
72    ///     signer,
73    ///     137,
74    ///     Some(creds),
75    ///     None,
76    /// );
77    ///
78    /// // Use the credentials for WebSocket authentication
79    /// if let Some(creds) = auth_client.api_creds() {
80    ///     let ws_client = UserWsClient::new();
81    ///     let mut stream = ws_client.subscribe_with_creds(creds).await?;
82    ///     // Process events...
83    /// }
84    /// # Ok(())
85    /// # }
86    /// ```
87    pub fn api_creds(&self) -> Option<&ApiCreds> {
88        self.api_creds.as_ref()
89    }
90
91    /// Set the API credentials
92    ///
93    /// Updates the API credentials for this client. This is useful when you want to:
94    /// - Initialize the client without credentials
95    /// - Fetch credentials later using `create_api_key()` or `derive_api_key()`
96    /// - Update credentials without recreating the client
97    ///
98    /// # Example
99    ///
100    /// ```no_run
101    /// # use polymarket_rs::AuthenticatedClient;
102    /// # use alloy_signer_local::PrivateKeySigner;
103    /// # #[tokio::main]
104    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
105    /// # let signer = PrivateKeySigner::random();
106    /// // Create client without credentials
107    /// let mut auth_client = AuthenticatedClient::new(
108    ///     "https://clob.polymarket.com",
109    ///     signer,
110    ///     137,
111    ///     None,  // No credentials initially
112    ///     None,
113    /// );
114    ///
115    /// // Fetch credentials using L1 authentication
116    /// let creds = auth_client.create_or_derive_api_key().await?;
117    ///
118    /// // Set the credentials
119    /// auth_client.set_api_creds(Some(creds));
120    ///
121    /// // Now you can use L2 authenticated methods
122    /// let keys = auth_client.get_api_keys().await?;
123    /// # Ok(())
124    /// # }
125    /// ```
126    pub fn set_api_creds(&mut self, api_creds: Option<ApiCreds>) {
127        self.api_creds = api_creds;
128    }
129
130    /// Create a new API key (L1 authentication required)
131    ///
132    /// This creates a new API key for the signer's address.
133    /// Requires wallet signature.
134    pub async fn create_api_key(&self, nonce: Option<U256>) -> Result<ApiCreds> {
135        let headers = create_l1_headers(&self.signer, self.chain_id, nonce)?;
136        self.http_client
137            .post("/auth/api-key", &serde_json::json!({}), Some(headers))
138            .await
139    }
140
141    /// Derive API key from existing credentials (L1 authentication required)
142    pub async fn derive_api_key(&self) -> Result<ApiCreds> {
143        let headers = create_l1_headers(&self.signer, self.chain_id, None)?;
144        self.http_client
145            .get("/auth/derive-api-key", Some(headers))
146            .await
147    }
148
149    /// Create or derive API key with fallback
150    ///
151    /// Tries to create a new API key, falls back to derive if creation fails.
152    pub async fn create_or_derive_api_key(&self) -> Result<ApiCreds> {
153        match self.create_api_key(None).await {
154            Ok(creds) => Ok(creds),
155            Err(_) => self.derive_api_key().await,
156        }
157    }
158
159    /// Get all API keys for the current user (L2 authentication required)
160    pub async fn get_api_keys(&self) -> Result<ApiKeysResponse> {
161        let api_creds = self
162            .api_creds
163            .as_ref()
164            .ok_or_else(|| Error::AuthRequired("API credentials required".to_string()))?;
165
166        let headers =
167            create_l2_headers::<_, ()>(&self.signer, api_creds, "GET", "/auth/api-keys", None)?;
168        self.http_client.get("/auth/api-keys", Some(headers)).await
169    }
170
171    /// Delete an API key (L2 authentication required)
172    pub async fn delete_api_key(&self) -> Result<serde_json::Value> {
173        let api_creds = self
174            .api_creds
175            .as_ref()
176            .ok_or_else(|| Error::AuthRequired("API credentials required".to_string()))?;
177
178        let headers =
179            create_l2_headers::<_, ()>(&self.signer, api_creds, "DELETE", "/auth/api-key", None)?;
180        self.http_client
181            .delete("/auth/api-key", Some(headers))
182            .await
183    }
184
185    /// Get balance and allowance information (L2 authentication required)
186    ///
187    /// # Arguments
188    /// * `params` - Query parameters for balance/allowance
189    pub async fn get_balance_allowance(
190        &self,
191        params: BalanceAllowanceParams,
192    ) -> Result<serde_json::Value> {
193        let api_creds = self
194            .api_creds
195            .as_ref()
196            .ok_or_else(|| Error::AuthRequired("API credentials required".to_string()))?;
197
198        // IMPORTANT: Sign the base path WITHOUT query parameters
199        let base_path = "/balance-allowance";
200        let headers = create_l2_headers::<_, ()>(&self.signer, api_creds, "GET", base_path, None)?;
201
202        // Build the full request path WITH query parameters
203        let query_params = params.to_query_params();
204        let request_path = if query_params.is_empty() {
205            base_path.to_string()
206        } else {
207            format!(
208                "{}?{}",
209                base_path,
210                query_params
211                    .iter()
212                    .map(|(k, v)| format!("{}={}", k, v))
213                    .collect::<Vec<_>>()
214                    .join("&")
215            )
216        };
217
218        self.http_client.get(&request_path, Some(headers)).await
219    }
220
221    /// Update balance allowance (L2 authentication required)
222    pub async fn update_balance_allowance(&self) -> Result<serde_json::Value> {
223        let api_creds = self
224            .api_creds
225            .as_ref()
226            .ok_or_else(|| Error::AuthRequired("API credentials required".to_string()))?;
227
228        let headers = create_l2_headers::<_, ()>(
229            &self.signer,
230            api_creds,
231            "GET",
232            "/balance-allowance/update",
233            None,
234        )?;
235        self.http_client
236            .get("/balance-allowance/update", Some(headers))
237            .await
238    }
239
240    /// Get notifications for the current user (L2 authentication required)
241    pub async fn get_notifications(&self) -> Result<serde_json::Value> {
242        let api_creds = self
243            .api_creds
244            .as_ref()
245            .ok_or_else(|| Error::AuthRequired("API credentials required".to_string()))?;
246
247        let headers =
248            create_l2_headers::<_, ()>(&self.signer, api_creds, "GET", "/notifications", None)?;
249        self.http_client.get("/notifications", Some(headers)).await
250    }
251
252    /// Drop (delete) notifications (L2 authentication required)
253    pub async fn drop_notifications(&self, ids: &[String]) -> Result<serde_json::Value> {
254        let api_creds = self
255            .api_creds
256            .as_ref()
257            .ok_or_else(|| Error::AuthRequired("API credentials required".to_string()))?;
258
259        let body = serde_json::json!({ "ids": ids });
260        let headers = create_l2_headers(
261            &self.signer,
262            api_creds,
263            "DELETE",
264            "/notifications",
265            Some(&body),
266        )?;
267        self.http_client
268            .delete_with_body("/notifications", &body, Some(headers))
269            .await
270    }
271
272    /// Get the signer's address
273    pub fn get_address(&self) -> String {
274        format!("{:?}", self.signer.address())
275    }
276
277    /// Get the funder address (for PolyProxy wallets)
278    ///
279    /// Returns the proxy wallet address if set, otherwise None.
280    /// For EOA wallets, this should return None.
281    pub fn get_funder(&self) -> Option<Address> {
282        self.funder
283    }
284}