use std::collections::HashMap;
use serde_json::{json, Value};
use crate::core::{
hmac_sha384, encode_base64, timestamp_millis,
Credentials, ExchangeResult,
};
#[derive(Clone)]
pub struct GeminiAuth {
api_key: String,
api_secret: String,
}
impl GeminiAuth {
pub fn new(credentials: &Credentials) -> ExchangeResult<Self> {
Ok(Self {
api_key: credentials.api_key.clone(),
api_secret: credentials.api_secret.clone(),
})
}
pub fn generate_nonce() -> u64 {
timestamp_millis()
}
pub fn sign_request(
&self,
endpoint: &str,
params: HashMap<String, Value>,
) -> ExchangeResult<HashMap<String, String>> {
let nonce = Self::generate_nonce();
let mut payload = json!({
"request": endpoint,
"nonce": nonce,
});
if let Some(obj) = payload.as_object_mut() {
for (key, value) in params {
obj.insert(key, value);
}
}
let payload_str = payload.to_string();
let b64_payload = encode_base64(payload_str.as_bytes());
let signature_bytes = hmac_sha384(
self.api_secret.as_bytes(),
b64_payload.as_bytes(),
);
let signature = hex::encode(signature_bytes);
let mut headers = HashMap::new();
headers.insert("X-GEMINI-APIKEY".to_string(), self.api_key.clone());
headers.insert("X-GEMINI-PAYLOAD".to_string(), b64_payload);
headers.insert("X-GEMINI-SIGNATURE".to_string(), signature);
headers.insert("Content-Type".to_string(), "text/plain".to_string());
headers.insert("Content-Length".to_string(), "0".to_string());
headers.insert("Cache-Control".to_string(), "no-cache".to_string());
Ok(headers)
}
pub fn sign_websocket_request(
&self,
endpoint: &str,
) -> ExchangeResult<HashMap<String, String>> {
self.sign_request(endpoint, HashMap::new())
}
pub fn api_key(&self) -> &str {
&self.api_key
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sign_request() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = GeminiAuth::new(&credentials).unwrap();
let headers = auth.sign_request("/v1/balances", HashMap::new()).unwrap();
assert!(headers.contains_key("X-GEMINI-APIKEY"));
assert!(headers.contains_key("X-GEMINI-PAYLOAD"));
assert!(headers.contains_key("X-GEMINI-SIGNATURE"));
assert_eq!(headers.get("X-GEMINI-APIKEY"), Some(&"test_key".to_string()));
assert_eq!(headers.get("Content-Type"), Some(&"text/plain".to_string()));
assert_eq!(headers.get("Content-Length"), Some(&"0".to_string()));
assert_eq!(headers.get("Cache-Control"), Some(&"no-cache".to_string()));
}
#[test]
fn test_signature_is_hex() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = GeminiAuth::new(&credentials).unwrap();
let headers = auth.sign_request("/v1/balances", HashMap::new()).unwrap();
let signature = headers.get("X-GEMINI-SIGNATURE").unwrap();
assert_eq!(signature.len(), 96);
assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_sign_with_params() {
let credentials = Credentials::new("test_key", "test_secret");
let auth = GeminiAuth::new(&credentials).unwrap();
let mut params = HashMap::new();
params.insert("symbol".to_string(), json!("btcusd"));
params.insert("amount".to_string(), json!("0.5"));
let headers = auth.sign_request("/v1/order/new", params).unwrap();
assert!(headers.contains_key("X-GEMINI-PAYLOAD"));
let b64_payload = headers.get("X-GEMINI-PAYLOAD").unwrap();
let payload_bytes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD,
b64_payload
).unwrap();
let payload_str = String::from_utf8(payload_bytes).unwrap();
assert!(payload_str.contains("btcusd"));
assert!(payload_str.contains("0.5"));
}
#[test]
fn test_generate_nonce() {
let nonce1 = GeminiAuth::generate_nonce();
std::thread::sleep(std::time::Duration::from_millis(2));
let nonce2 = GeminiAuth::generate_nonce();
assert!(nonce2 > nonce1);
assert!(nonce1 > 1_600_000_000_000);
}
}