polyte_clob/account/mod.rs
1//! Account module for credential management and signing operations.
2//!
3//! This module provides a unified abstraction for managing Polymarket CLOB authentication,
4//! including wallet management, API credentials, and signing operations.
5
6mod credentials;
7mod signer;
8mod wallet;
9
10use std::path::Path;
11
12use alloy::primitives::Address;
13pub use credentials::Credentials;
14use serde::{Deserialize, Serialize};
15pub use signer::Signer;
16pub use wallet::Wallet;
17
18use crate::{
19 core::eip712::{sign_clob_auth, sign_order},
20 error::{ClobError, Result},
21 types::{Order, SignedOrder},
22};
23
24/// Environment variable names for account configuration
25pub mod env {
26 pub const PRIVATE_KEY: &str = "POLYMARKET_PRIVATE_KEY";
27 pub const API_KEY: &str = "POLYMARKET_API_KEY";
28 pub const API_SECRET: &str = "POLYMARKET_API_SECRET";
29 pub const API_PASSPHRASE: &str = "POLYMARKET_API_PASSPHRASE";
30}
31
32/// Account configuration for file-based loading
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct AccountConfig {
35 pub private_key: String,
36 #[serde(flatten)]
37 pub credentials: Credentials,
38}
39
40/// Unified account primitive for credential management and signing operations.
41///
42/// `Account` combines wallet (private key), API credentials, and signing capabilities
43/// into a single abstraction. It provides factory methods for loading credentials from
44/// various sources (environment variables, files) and handles both EIP-712 order signing
45/// and HMAC-based L2 API authentication.
46///
47/// # Example
48///
49/// ```no_run
50/// use polyte_clob::Account;
51///
52/// // Load from environment variables
53/// let account = Account::from_env()?;
54///
55/// // Or load from a JSON file
56/// let account = Account::from_file("config/account.json")?;
57///
58/// // Get the wallet address
59/// println!("Address: {:?}", account.address());
60/// # Ok::<(), polyte_clob::ClobError>(())
61/// ```
62#[derive(Clone, Debug)]
63pub struct Account {
64 wallet: Wallet,
65 credentials: Credentials,
66 signer: Signer,
67}
68
69impl Account {
70 /// Create a new account from private key and credentials.
71 ///
72 /// # Arguments
73 ///
74 /// * `private_key` - Hex-encoded private key (with or without 0x prefix)
75 /// * `credentials` - API credentials for L2 authentication
76 ///
77 /// # Example
78 ///
79 /// ```no_run
80 /// use polyte_clob::{Account, Credentials};
81 ///
82 /// let credentials = Credentials {
83 /// key: "api_key".to_string(),
84 /// secret: "api_secret".to_string(),
85 /// passphrase: "passphrase".to_string(),
86 /// };
87 ///
88 /// let account = Account::new("0x...", credentials)?;
89 /// # Ok::<(), polyte_clob::ClobError>(())
90 /// ```
91 pub fn new(private_key: impl Into<String>, credentials: Credentials) -> Result<Self> {
92 let wallet = Wallet::from_private_key(&private_key.into())?;
93 let signer = Signer::new(&credentials.secret)?;
94
95 Ok(Self {
96 wallet,
97 credentials,
98 signer,
99 })
100 }
101
102 /// Load account from environment variables.
103 ///
104 /// Reads the following environment variables:
105 /// - `POLYMARKET_PRIVATE_KEY`: Hex-encoded private key
106 /// - `POLYMARKET_API_KEY`: API key
107 /// - `POLYMARKET_API_SECRET`: API secret (base64 encoded)
108 /// - `POLYMARKET_API_PASSPHRASE`: API passphrase
109 ///
110 /// # Example
111 ///
112 /// ```no_run
113 /// use polyte_clob::Account;
114 ///
115 /// let account = Account::from_env()?;
116 /// # Ok::<(), polyte_clob::ClobError>(())
117 /// ```
118 pub fn from_env() -> Result<Self> {
119 let private_key = std::env::var(env::PRIVATE_KEY).map_err(|_| {
120 ClobError::validation(format!(
121 "Missing environment variable: {}",
122 env::PRIVATE_KEY
123 ))
124 })?;
125
126 let credentials = Credentials {
127 key: std::env::var(env::API_KEY).map_err(|_| {
128 ClobError::validation(format!("Missing environment variable: {}", env::API_KEY))
129 })?,
130 secret: std::env::var(env::API_SECRET).map_err(|_| {
131 ClobError::validation(format!("Missing environment variable: {}", env::API_SECRET))
132 })?,
133 passphrase: std::env::var(env::API_PASSPHRASE).map_err(|_| {
134 ClobError::validation(format!(
135 "Missing environment variable: {}",
136 env::API_PASSPHRASE
137 ))
138 })?,
139 };
140
141 Self::new(private_key, credentials)
142 }
143
144 /// Load account from a JSON configuration file.
145 ///
146 /// The file should contain:
147 /// ```json
148 /// {
149 /// "private_key": "0x...",
150 /// "key": "api_key",
151 /// "secret": "api_secret",
152 /// "passphrase": "passphrase"
153 /// }
154 /// ```
155 ///
156 /// # Example
157 ///
158 /// ```no_run
159 /// use polyte_clob::Account;
160 ///
161 /// let account = Account::from_file("config/account.json")?;
162 /// # Ok::<(), polyte_clob::ClobError>(())
163 /// ```
164 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
165 let path = path.as_ref();
166 let content = std::fs::read_to_string(path).map_err(|e| {
167 ClobError::validation(format!(
168 "Failed to read config file {}: {}",
169 path.display(),
170 e
171 ))
172 })?;
173
174 Self::from_json(&content)
175 }
176
177 /// Load account from a JSON string.
178 ///
179 /// # Example
180 ///
181 /// ```no_run
182 /// use polyte_clob::Account;
183 ///
184 /// let json = r#"{
185 /// "private_key": "0x...",
186 /// "key": "api_key",
187 /// "secret": "api_secret",
188 /// "passphrase": "passphrase"
189 /// }"#;
190 ///
191 /// let account = Account::from_json(json)?;
192 /// # Ok::<(), polyte_clob::ClobError>(())
193 /// ```
194 pub fn from_json(json: &str) -> Result<Self> {
195 let config: AccountConfig = serde_json::from_str(json)
196 .map_err(|e| ClobError::validation(format!("Failed to parse JSON config: {}", e)))?;
197
198 Self::new(config.private_key, config.credentials)
199 }
200
201 /// Get the wallet address.
202 pub fn address(&self) -> Address {
203 self.wallet.address()
204 }
205
206 /// Get a reference to the wallet.
207 pub fn wallet(&self) -> &Wallet {
208 &self.wallet
209 }
210
211 /// Get a reference to the credentials.
212 pub fn credentials(&self) -> &Credentials {
213 &self.credentials
214 }
215
216 /// Get a reference to the HMAC signer.
217 pub fn signer(&self) -> &Signer {
218 &self.signer
219 }
220
221 /// Sign an order using EIP-712.
222 ///
223 /// # Arguments
224 ///
225 /// * `order` - The unsigned order to sign
226 /// * `chain_id` - The chain ID for EIP-712 domain
227 ///
228 /// # Example
229 ///
230 /// ```no_run
231 /// use polyte_clob::{Account, Order};
232 ///
233 /// async fn example(account: &Account, order: &Order) -> Result<(), Box<dyn std::error::Error>> {
234 /// let signed_order = account.sign_order(order, 137).await?;
235 /// println!("Signature: {}", signed_order.signature);
236 /// Ok(())
237 /// }
238 /// ```
239 pub async fn sign_order(&self, order: &Order, chain_id: u64) -> Result<SignedOrder> {
240 let signature = sign_order(order, self.wallet.signer(), chain_id).await?;
241
242 Ok(SignedOrder {
243 order: order.clone(),
244 signature,
245 })
246 }
247
248 /// Sign a CLOB authentication message for API key creation (L1 auth).
249 ///
250 /// # Arguments
251 ///
252 /// * `chain_id` - The chain ID for EIP-712 domain
253 /// * `timestamp` - Unix timestamp in seconds
254 /// * `nonce` - Random nonce value
255 pub async fn sign_clob_auth(
256 &self,
257 chain_id: u64,
258 timestamp: u64,
259 nonce: u32,
260 ) -> Result<String> {
261 sign_clob_auth(self.wallet.signer(), chain_id, timestamp, nonce).await
262 }
263
264 /// Sign an L2 API request message using HMAC.
265 ///
266 /// # Arguments
267 ///
268 /// * `timestamp` - Unix timestamp in seconds
269 /// * `method` - HTTP method (GET, POST, DELETE)
270 /// * `path` - Request path (e.g., "/order")
271 /// * `body` - Optional request body
272 pub fn sign_l2_request(
273 &self,
274 timestamp: u64,
275 method: &str,
276 path: &str,
277 body: Option<&str>,
278 ) -> Result<String> {
279 let message = Signer::create_message(timestamp, method, path, body);
280 self.signer.sign(&message)
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn test_from_json() {
290 let json = r#"{
291 "private_key": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
292 "key": "test_key",
293 "secret": "c2VjcmV0",
294 "passphrase": "test_pass"
295 }"#;
296
297 let account = Account::from_json(json).unwrap();
298 assert_eq!(account.credentials().key, "test_key");
299 assert_eq!(account.credentials().passphrase, "test_pass");
300 }
301
302 #[test]
303 fn test_sign_l2_request() {
304 let json = r#"{
305 "private_key": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
306 "key": "test_key",
307 "secret": "c2VjcmV0",
308 "passphrase": "test_pass"
309 }"#;
310
311 let account = Account::from_json(json).unwrap();
312 let signature = account
313 .sign_l2_request(1234567890, "GET", "/api/test", None)
314 .unwrap();
315
316 // Should be URL-safe base64
317 assert!(!signature.contains('+'));
318 assert!(!signature.contains('/'));
319 }
320}