use std::collections::HashMap;
use std::time::{Duration, Instant};
use crate::core::{
hmac_sha256, encode_hex, timestamp_millis,
Credentials, ExchangeResult, ExchangeError,
};
#[derive(Clone)]
pub struct DeribitAuth {
client_id: String,
client_secret: String,
access_token: Option<String>,
refresh_token: Option<String>,
expires_at: Option<Instant>,
}
impl DeribitAuth {
pub fn new(credentials: &Credentials) -> ExchangeResult<Self> {
Ok(Self {
client_id: credentials.api_key.clone(),
client_secret: credentials.api_secret.clone(),
access_token: None,
refresh_token: None,
expires_at: None,
})
}
pub fn has_valid_token(&self) -> bool {
if let (Some(_token), Some(expires)) = (&self.access_token, self.expires_at) {
expires > Instant::now() + Duration::from_secs(60)
} else {
false
}
}
pub fn access_token(&self) -> Option<&str> {
if self.has_valid_token() {
self.access_token.as_deref()
} else {
None
}
}
pub fn refresh_token(&self) -> Option<&str> {
self.refresh_token.as_deref()
}
pub fn store_tokens(
&mut self,
access_token: String,
refresh_token: String,
expires_in: u64,
) {
self.access_token = Some(access_token);
self.refresh_token = Some(refresh_token);
self.expires_at = Some(Instant::now() + Duration::from_secs(expires_in));
}
pub fn clear_tokens(&mut self) {
self.access_token = None;
self.refresh_token = None;
self.expires_at = None;
}
pub fn client_credentials_params(&self) -> HashMap<String, serde_json::Value> {
let mut params = HashMap::new();
params.insert("grant_type".to_string(), serde_json::json!("client_credentials"));
params.insert("client_id".to_string(), serde_json::json!(self.client_id));
params.insert("client_secret".to_string(), serde_json::json!(self.client_secret));
params
}
pub fn client_signature_params(&self) -> HashMap<String, serde_json::Value> {
let timestamp = timestamp_millis();
let nonce = generate_nonce();
let data = "";
let message = format!("{}\n{}\n{}", timestamp, nonce, data);
let signature_bytes = hmac_sha256(
self.client_secret.as_bytes(),
message.as_bytes(),
);
let signature = encode_hex(&signature_bytes);
let mut params = HashMap::new();
params.insert("grant_type".to_string(), serde_json::json!("client_signature"));
params.insert("client_id".to_string(), serde_json::json!(self.client_id));
params.insert("timestamp".to_string(), serde_json::json!(timestamp));
params.insert("nonce".to_string(), serde_json::json!(nonce));
params.insert("signature".to_string(), serde_json::json!(signature));
params.insert("data".to_string(), serde_json::json!(data));
params
}
pub fn refresh_token_params(&self) -> ExchangeResult<HashMap<String, serde_json::Value>> {
let refresh_token = self.refresh_token.as_ref()
.ok_or_else(|| ExchangeError::Auth("No refresh token available".to_string()))?;
let mut params = HashMap::new();
params.insert("grant_type".to_string(), serde_json::json!("refresh_token"));
params.insert("refresh_token".to_string(), serde_json::json!(refresh_token));
Ok(params)
}
pub fn auth_header(&self) -> ExchangeResult<String> {
let token = self.access_token()
.ok_or_else(|| ExchangeError::Auth("No valid access token".to_string()))?;
Ok(format!("Bearer {}", token))
}
pub fn client_id(&self) -> &str {
&self.client_id
}
}
fn generate_nonce() -> String {
use rand::Rng;
const CHARSET: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const NONCE_LEN: usize = 16;
let mut rng = rand::thread_rng();
(0..NONCE_LEN)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_credentials_params() {
let credentials = Credentials::new("test_client_id", "test_client_secret");
let auth = DeribitAuth::new(&credentials).unwrap();
let params = auth.client_credentials_params();
assert_eq!(params.get("grant_type").unwrap(), "client_credentials");
assert_eq!(params.get("client_id").unwrap(), "test_client_id");
assert_eq!(params.get("client_secret").unwrap(), "test_client_secret");
}
#[test]
fn test_client_signature_params() {
let credentials = Credentials::new("test_client_id", "test_client_secret");
let auth = DeribitAuth::new(&credentials).unwrap();
let params = auth.client_signature_params();
assert_eq!(params.get("grant_type").unwrap(), "client_signature");
assert_eq!(params.get("client_id").unwrap(), "test_client_id");
assert!(params.contains_key("timestamp"));
assert!(params.contains_key("nonce"));
assert!(params.contains_key("signature"));
assert!(params.contains_key("data"));
let signature = params.get("signature").unwrap().as_str().unwrap();
assert_eq!(signature.len(), 64);
assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_token_lifecycle() {
let credentials = Credentials::new("test_id", "test_secret");
let mut auth = DeribitAuth::new(&credentials).unwrap();
assert!(!auth.has_valid_token());
assert!(auth.access_token().is_none());
auth.store_tokens(
"test_access_token".to_string(),
"test_refresh_token".to_string(),
900, );
assert!(auth.has_valid_token());
assert_eq!(auth.access_token(), Some("test_access_token"));
assert_eq!(auth.refresh_token(), Some("test_refresh_token"));
auth.clear_tokens();
assert!(!auth.has_valid_token());
assert!(auth.access_token().is_none());
}
#[test]
fn test_auth_header() {
let credentials = Credentials::new("test_id", "test_secret");
let mut auth = DeribitAuth::new(&credentials).unwrap();
assert!(auth.auth_header().is_err());
auth.store_tokens(
"my_jwt_token".to_string(),
"my_refresh_token".to_string(),
900,
);
let header = auth.auth_header().unwrap();
assert_eq!(header, "Bearer my_jwt_token");
}
#[test]
fn test_generate_nonce() {
let nonce1 = generate_nonce();
let nonce2 = generate_nonce();
assert_eq!(nonce1.len(), 16);
assert_eq!(nonce2.len(), 16);
assert_ne!(nonce1, nonce2);
assert!(nonce1.chars().all(|c| c.is_alphanumeric()));
}
}