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