mod credentials;
mod signer;
mod wallet;
use std::path::Path;
use alloy::primitives::Address;
pub use credentials::Credentials;
use serde::{Deserialize, Serialize};
pub use signer::Signer;
pub use wallet::Wallet;
use crate::{
core::eip712::{sign_clob_auth, sign_order},
error::ClobError,
types::{Order, SignedOrder},
};
pub mod env {
pub const PRIVATE_KEY: &str = "POLYMARKET_PRIVATE_KEY";
pub const API_KEY: &str = "POLYMARKET_API_KEY";
pub const API_SECRET: &str = "POLYMARKET_API_SECRET";
pub const API_PASSPHRASE: &str = "POLYMARKET_API_PASSPHRASE";
}
#[derive(Clone, Serialize, Deserialize)]
pub struct AccountConfig {
pub private_key: String,
#[serde(flatten)]
pub credentials: Credentials,
}
impl std::fmt::Debug for AccountConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AccountConfig")
.field("private_key", &"[REDACTED]")
.field("credentials", &self.credentials)
.finish()
}
}
#[derive(Clone, Debug)]
pub struct Account {
wallet: Wallet,
credentials: Credentials,
signer: Signer,
}
impl Account {
pub fn new(
private_key: impl Into<String>,
credentials: Credentials,
) -> Result<Self, ClobError> {
let wallet = Wallet::from_private_key(&private_key.into())?;
let signer = Signer::new(&credentials.secret);
Ok(Self {
wallet,
credentials,
signer,
})
}
pub fn from_env() -> Result<Self, ClobError> {
let private_key = std::env::var(env::PRIVATE_KEY).map_err(|_| {
ClobError::validation(format!(
"Missing environment variable: {}",
env::PRIVATE_KEY
))
})?;
let credentials = Credentials {
key: std::env::var(env::API_KEY).map_err(|_| {
ClobError::validation(format!("Missing environment variable: {}", env::API_KEY))
})?,
secret: std::env::var(env::API_SECRET).map_err(|_| {
ClobError::validation(format!("Missing environment variable: {}", env::API_SECRET))
})?,
passphrase: std::env::var(env::API_PASSPHRASE).map_err(|_| {
ClobError::validation(format!(
"Missing environment variable: {}",
env::API_PASSPHRASE
))
})?,
};
Self::new(private_key, credentials)
}
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ClobError> {
let path = path.as_ref();
let content = std::fs::read_to_string(path).map_err(|e| {
ClobError::validation(format!(
"Failed to read config file {}: {}",
path.display(),
e
))
})?;
Self::from_json(&content)
}
pub fn from_json(json: &str) -> Result<Self, ClobError> {
let config: AccountConfig = serde_json::from_str(json)
.map_err(|e| ClobError::validation(format!("Failed to parse JSON config: {}", e)))?;
Self::new(config.private_key, config.credentials)
}
pub fn address(&self) -> Address {
self.wallet.address()
}
pub fn wallet(&self) -> &Wallet {
&self.wallet
}
pub fn credentials(&self) -> &Credentials {
&self.credentials
}
pub fn signer(&self) -> &Signer {
&self.signer
}
pub async fn sign_order(&self, order: &Order, chain_id: u64) -> Result<SignedOrder, ClobError> {
let signature = sign_order(order, self.wallet.signer(), chain_id).await?;
Ok(SignedOrder {
order: order.clone(),
signature,
})
}
pub async fn sign_clob_auth(
&self,
chain_id: u64,
timestamp: u64,
nonce: u32,
) -> Result<String, ClobError> {
sign_clob_auth(self.wallet.signer(), chain_id, timestamp, nonce).await
}
pub fn sign_l2_request(
&self,
timestamp: u64,
method: &str,
path: &str,
body: Option<&str>,
) -> Result<String, ClobError> {
let message = Signer::create_message(timestamp, method, path, body);
self.signer.sign(&message)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_json() {
let json = r#"{
"private_key": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
"key": "test_key",
"secret": "c2VjcmV0",
"passphrase": "test_pass"
}"#;
let account = Account::from_json(json).unwrap();
assert_eq!(account.credentials().key, "test_key");
assert_eq!(account.credentials().passphrase, "test_pass");
}
#[test]
fn test_account_config_debug_redacts_private_key() {
let config = AccountConfig {
private_key: "0xdeadbeef_super_secret_key".to_string(),
credentials: Credentials {
key: "api_key".to_string(),
secret: "api_secret".to_string(),
passphrase: "pass".to_string(),
},
};
let debug_output = format!("{:?}", config);
assert!(
debug_output.contains("[REDACTED]"),
"Debug should contain [REDACTED], got: {debug_output}"
);
assert!(
!debug_output.contains("deadbeef"),
"Debug should not contain the private key, got: {debug_output}"
);
}
#[test]
fn test_sign_l2_request() {
let json = r#"{
"private_key": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
"key": "test_key",
"secret": "c2VjcmV0",
"passphrase": "test_pass"
}"#;
let account = Account::from_json(json).unwrap();
let signature = account
.sign_l2_request(1234567890, "GET", "/api/test", None)
.unwrap();
assert!(!signature.contains('+'));
assert!(!signature.contains('/'));
}
}