polyte_clob/account/
signer.rs1use 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#[derive(Clone, Debug)]
9pub struct Signer {
10 secret: Vec<u8>,
11}
12
13impl Signer {
14 pub fn new(secret: &str) -> Result<Self> {
16 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 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 let signature = signature.replace('+', "-").replace('/', "_");
41
42 Ok(signature)
43 }
44
45 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 let secret = "c2VjcmV0"; 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 assert!(!signature.contains('+'));
66 assert!(!signature.contains('/'));
67 }
68}