polyte_clob/account/
signer.rs

1use base64::{engine::general_purpose::STANDARD, prelude::BASE64_URL_SAFE_NO_PAD, Engine};
2use hmac::{Hmac, Mac};
3use sha2::Sha256;
4
5use crate::error::{ClobError, Result};
6
7/// HMAC signer for API authentication
8#[derive(Clone, Debug)]
9pub struct Signer {
10    secret: Vec<u8>,
11}
12
13impl Signer {
14    /// Create a new signer from base64-encoded secret (supports multiple formats)
15    pub fn new(secret: &str) -> Result<Self> {
16        // Try different base64 formats:
17        // 1. URL-safe without padding (most common for API keys)
18        // 2. URL-safe with padding
19        // 3. Standard base64
20        // 4. Use raw bytes if all else fails
21        let decoded = BASE64_URL_SAFE_NO_PAD
22            .decode(secret)
23            .or_else(|_| base64::engine::general_purpose::URL_SAFE.decode(secret))
24            .or_else(|_| STANDARD.decode(secret))
25            .unwrap_or_else(|_| secret.as_bytes().to_vec());
26
27        Ok(Self { secret: decoded })
28    }
29
30    /// Sign a message with HMAC-SHA256
31    pub fn sign(&self, message: &str) -> Result<String> {
32        let mut mac = Hmac::<Sha256>::new_from_slice(&self.secret)
33            .map_err(|e| ClobError::Crypto(format!("Failed to create HMAC: {}", e)))?;
34
35        mac.update(message.as_bytes());
36        let result = mac.finalize();
37        let signature = STANDARD.encode(result.into_bytes());
38
39        // Convert to URL-safe base64
40        let signature = signature.replace('+', "-").replace('/', "_");
41
42        Ok(signature)
43    }
44
45    /// Create signature message for API request
46    pub fn create_message(timestamp: u64, method: &str, path: &str, body: Option<&str>) -> String {
47        format!("{}{}{}{}", timestamp, method, path, body.unwrap_or(""))
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn test_sign() {
57        // Test secret (base64)
58        let secret = "c2VjcmV0"; // "secret" in base64
59        let signer = Signer::new(secret).unwrap();
60
61        let message = Signer::create_message(1234567890, "GET", "/api/test", None);
62        let signature = signer.sign(&message).unwrap();
63
64        // Signature should be URL-safe base64
65        assert!(!signature.contains('+'));
66        assert!(!signature.contains('/'));
67    }
68}