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