use base64::{engine::general_purpose::URL_SAFE, Engine};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use crate::clob::{ClobError, Result};
type HmacSha256 = Hmac<Sha256>;
pub fn build_hmac_signature(
secret: &str,
timestamp: &str,
method: &str,
request_path: &str,
body: Option<&str>,
) -> Result<String> {
if secret.is_empty() {
return Err(ClobError::SigningError(
"API secret is empty. Set CLOB_API_SECRET or derive credentials first.".to_string()
));
}
let secret_bytes = URL_SAFE.decode(secret)
.map_err(|e| {
ClobError::SigningError(format!(
"Invalid API secret format (must be base64): {}. Secret length: {}",
e,
secret.len()
))
})?;
let mut message = format!("{}{}{}", timestamp, method, request_path);
if let Some(body_str) = body {
message.push_str(&body_str.replace('\'', "\""));
}
let mut mac = HmacSha256::new_from_slice(&secret_bytes)
.map_err(|e| ClobError::SigningError(e.to_string()))?;
mac.update(message.as_bytes());
let result = mac.finalize();
let signature_bytes = result.into_bytes();
Ok(URL_SAFE.encode(signature_bytes))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hmac_signature() {
let secret = "dGVzdHNlY3JldA=="; let timestamp = "1234567890";
let method = "GET";
let path = "/test";
let sig = build_hmac_signature(secret, timestamp, method, path, None);
assert!(sig.is_ok());
}
#[test]
fn test_hmac_with_body() {
let secret = "dGVzdHNlY3JldA=="; let timestamp = "1234567890";
let method = "POST";
let path = "/test";
let body = r#"{"key":"value"}"#;
let sig = build_hmac_signature(secret, timestamp, method, path, Some(body));
assert!(sig.is_ok());
}
#[test]
fn test_hmac_matches_python() {
let secret = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
let timestamp = "1000000";
let method = "test-sign";
let path = "/orders";
let body = r#"{"hash": "0x123"}"#;
let signature = build_hmac_signature(secret, timestamp, method, path, Some(body)).unwrap();
let expected = "ZwAdJKvoYRlEKDkNMwd5BuwNNtg93kNaR_oU2HrfVvc=";
assert_eq!(
signature, expected,
"HMAC signature should match Python implementation"
);
}
}