use std::time::{SystemTime, UNIX_EPOCH};
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use crate::config::NdaxCredentials;
type HmacSha256 = Hmac<Sha256>;
pub fn generate_signature(
nonce: &str,
user_id: &str,
api_key: &str,
api_secret: &str,
) -> String {
let message = format!("{}{}{}", nonce, user_id, api_key);
let mut mac = HmacSha256::new_from_slice(api_secret.as_bytes())
.expect("HMAC can take key of any size");
mac.update(message.as_bytes());
let result = mac.finalize().into_bytes();
hex::encode(result)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthenticateUserRequest {
#[serde(rename = "APIKey")]
pub api_key: String,
#[serde(rename = "Signature")]
pub signature: String,
#[serde(rename = "UserId")]
pub user_id: String,
#[serde(rename = "Nonce")]
pub nonce: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthenticateUserResponse {
#[serde(alias = "Authenticated")]
pub authenticated: bool,
#[serde(alias = "Locked")]
pub locked: bool,
#[serde(rename = "requires2FA", alias = "Requires2FA")]
pub requires_2fa: bool,
#[serde(rename = "twoFAType", alias = "TwoFAType")]
pub two_fa_type: Option<String>,
#[serde(rename = "twoFAToken", alias = "TwoFAToken")]
pub two_fa_token: Option<String>,
#[serde(alias = "User")]
pub user: Option<UserInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInfo {
#[serde(rename = "UserId", alias = "userId")]
pub user_id: i64,
#[serde(rename = "UserName", alias = "userName")]
pub user_name: String,
#[serde(rename = "Email", alias = "email")]
pub email: String,
#[serde(rename = "EmailVerified", alias = "emailVerified")]
pub email_verified: bool,
#[serde(rename = "AccountId", alias = "accountId")]
pub account_id: i64,
#[serde(rename = "OMSId", alias = "omsId")]
pub oms_id: i64,
#[serde(rename = "Use2FA", alias = "use2FA")]
pub use_2fa: bool,
}
pub fn create_auth_request(
credentials: &NdaxCredentials,
) -> AuthenticateUserRequest {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_millis()
.to_string();
let signature = generate_signature(
&nonce,
&credentials.user_id,
&credentials.api_key,
&credentials.api_secret,
);
AuthenticateUserRequest {
api_key: credentials.api_key.clone(),
signature,
user_id: credentials.user_id.clone(),
nonce,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_signature_known_values() {
let nonce = "1234567890123";
let user_id = "123456";
let api_key = "00000000000000000000000000000000";
let api_secret = "11111111111111111111111111111111";
let signature = generate_signature(nonce, user_id, api_key, api_secret);
assert_eq!(signature.len(), 64);
assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
assert_eq!(signature, signature.to_lowercase());
let signature2 = generate_signature(nonce, user_id, api_key, api_secret);
assert_eq!(signature, signature2);
assert_eq!(
signature,
"d064ba829f7310653796770eb33f557deedebabb591d546f0add3600fab1ccda"
);
}
#[test]
fn test_auth_request_serialize() {
let request = AuthenticateUserRequest {
api_key: "test_key".to_string(),
signature: "abc123".to_string(),
user_id: "12345".to_string(),
nonce: "1709123456789".to_string(),
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"APIKey\""));
assert!(json.contains("\"Signature\""));
assert!(json.contains("\"UserId\""));
assert!(json.contains("\"Nonce\""));
assert!(json.contains("\"test_key\""));
assert!(json.contains("\"abc123\""));
assert!(json.contains("\"12345\""));
assert!(json.contains("\"1709123456789\""));
}
#[test]
fn test_auth_response_deserialize() {
let json = r#"{
"authenticated": true,
"locked": false,
"requires2FA": false,
"twoFAType": null,
"twoFAToken": null,
"user": {
"UserId": 12345,
"UserName": "testuser",
"Email": "test@example.com",
"EmailVerified": true,
"AccountId": 67890,
"OMSId": 1,
"Use2FA": false
}
}"#;
let response: AuthenticateUserResponse =
serde_json::from_str(json).unwrap();
assert!(response.authenticated);
assert!(!response.locked);
assert!(!response.requires_2fa);
assert!(response.two_fa_type.is_none());
assert!(response.two_fa_token.is_none());
let user = response.user.unwrap();
assert_eq!(user.user_id, 12345);
assert_eq!(user.user_name, "testuser");
assert_eq!(user.email, "test@example.com");
assert!(user.email_verified);
assert_eq!(user.account_id, 67890);
assert_eq!(user.oms_id, 1);
assert!(!user.use_2fa);
}
#[test]
fn test_create_auth_request() {
let credentials =
NdaxCredentials::new("my_api_key", "my_api_secret", "12345");
let request = create_auth_request(&credentials);
assert_eq!(request.api_key, "my_api_key");
assert_eq!(request.user_id, "12345");
let nonce: u128 = request.nonce.parse().unwrap();
assert!(nonce > 1700000000000); assert_eq!(request.signature.len(), 64);
assert!(request.signature.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_user_info_serialize_deserialize() {
let user = UserInfo {
user_id: 12345,
user_name: "testuser".to_string(),
email: "test@example.com".to_string(),
email_verified: true,
account_id: 67890,
oms_id: 1,
use_2fa: false,
};
let json = serde_json::to_string(&user).unwrap();
assert!(json.contains("\"UserId\""));
assert!(json.contains("\"UserName\""));
assert!(json.contains("\"Email\""));
assert!(json.contains("\"EmailVerified\""));
assert!(json.contains("\"AccountId\""));
assert!(json.contains("\"OMSId\""));
assert!(json.contains("\"Use2FA\""));
let deserialized: UserInfo = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.user_id, user.user_id);
assert_eq!(deserialized.user_name, user.user_name);
}
}