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}