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}