1use std::{
19 collections::HashMap,
20 fmt::Debug,
21 sync::atomic::{AtomicU64, Ordering},
22 time::{SystemTime, UNIX_EPOCH},
23};
24
25use aws_lc_rs::{digest, hmac};
26use base64::{Engine, engine::general_purpose::STANDARD};
27use nautilus_core::{env::resolve_env_var_pair, string::secret::REDACTED};
28use serde_urlencoded;
29use zeroize::{Zeroize, ZeroizeOnDrop};
30
31use crate::common::enums::{KrakenEnvironment, KrakenProductType};
32
33static NONCE_COUNTER: AtomicU64 = AtomicU64::new(0);
35
36#[must_use]
39pub fn credential_env_vars(
40 product_type: KrakenProductType,
41 environment: KrakenEnvironment,
42) -> (&'static str, &'static str) {
43 match product_type {
44 KrakenProductType::Spot => ("KRAKEN_SPOT_API_KEY", "KRAKEN_SPOT_API_SECRET"),
45 KrakenProductType::Futures => {
46 if matches!(environment, KrakenEnvironment::Demo) {
47 (
48 "KRAKEN_FUTURES_DEMO_API_KEY",
49 "KRAKEN_FUTURES_DEMO_API_SECRET",
50 )
51 } else {
52 ("KRAKEN_FUTURES_API_KEY", "KRAKEN_FUTURES_API_SECRET")
53 }
54 }
55 }
56}
57
58#[derive(Clone, Zeroize, ZeroizeOnDrop)]
60pub struct KrakenCredential {
61 api_key: String,
62 api_secret: String,
63}
64
65impl Debug for KrakenCredential {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.debug_struct(stringify!(KrakenCredential))
68 .field("api_key", &self.api_key)
69 .field("api_secret", &REDACTED)
70 .finish()
71 }
72}
73
74impl KrakenCredential {
75 pub fn new(api_key: impl Into<String>, api_secret: impl Into<String>) -> Self {
77 Self {
78 api_key: api_key.into(),
79 api_secret: api_secret.into(),
80 }
81 }
82
83 #[must_use]
91 pub fn from_env_spot() -> Option<Self> {
92 let (key_var, secret_var) =
93 credential_env_vars(KrakenProductType::Spot, KrakenEnvironment::Mainnet);
94 let (k, s) = resolve_env_var_pair(None, None, key_var, secret_var)?;
95 Some(Self::new(k, s))
96 }
97
98 #[must_use]
105 pub fn from_env_futures(demo: bool) -> Option<Self> {
106 let environment = if demo {
107 KrakenEnvironment::Demo
108 } else {
109 KrakenEnvironment::Mainnet
110 };
111 let (key_var, secret_var) = credential_env_vars(KrakenProductType::Futures, environment);
112 let (k, s) = resolve_env_var_pair(None, None, key_var, secret_var)?;
113 Some(Self::new(k, s))
114 }
115
116 #[must_use]
121 pub fn resolve_spot(api_key: Option<String>, api_secret: Option<String>) -> Option<Self> {
122 let (key_var, secret_var) =
123 credential_env_vars(KrakenProductType::Spot, KrakenEnvironment::Mainnet);
124 let (k, s) = resolve_env_var_pair(api_key, api_secret, key_var, secret_var)?;
125 Some(Self::new(k, s))
126 }
127
128 #[must_use]
133 pub fn resolve_futures(
134 api_key: Option<String>,
135 api_secret: Option<String>,
136 demo: bool,
137 ) -> Option<Self> {
138 let environment = if demo {
139 KrakenEnvironment::Demo
140 } else {
141 KrakenEnvironment::Mainnet
142 };
143 let (key_var, secret_var) = credential_env_vars(KrakenProductType::Futures, environment);
144 let (k, s) = resolve_env_var_pair(api_key, api_secret, key_var, secret_var)?;
145 Some(Self::new(k, s))
146 }
147
148 pub fn api_key(&self) -> &str {
150 &self.api_key
151 }
152
153 pub fn into_parts(&self) -> (String, String) {
155 (self.api_key.clone(), self.api_secret.clone())
156 }
157
158 pub fn sign_spot(
167 &self,
168 path: &str,
169 nonce: u64,
170 params: &HashMap<String, String>,
171 ) -> anyhow::Result<(String, String)> {
172 let secret = STANDARD
173 .decode(&self.api_secret)
174 .map_err(|e| anyhow::anyhow!("Failed to decode API secret: {e}"))?;
175
176 let nonce_str = nonce.to_string();
177 let mut post_data = format!("nonce={nonce_str}");
178
179 if !params.is_empty() {
180 let encoded = serde_urlencoded::to_string(params)
181 .map_err(|e| anyhow::anyhow!("Failed to encode params: {e}"))?;
182 post_data.push('&');
183 post_data.push_str(&encoded);
184 }
185
186 let sha_input = format!("{nonce_str}{post_data}");
187 let hash = digest::digest(&digest::SHA256, sha_input.as_bytes());
188 let mut message = path.as_bytes().to_vec();
189 message.extend_from_slice(hash.as_ref());
190 let key = hmac::Key::new(hmac::HMAC_SHA512, &secret);
191 let signature = hmac::sign(&key, &message);
192
193 Ok((STANDARD.encode(signature.as_ref()), post_data))
194 }
195
196 pub fn sign_spot_json(
201 &self,
202 path: &str,
203 nonce: u64,
204 json_body: &str,
205 ) -> anyhow::Result<String> {
206 let secret = STANDARD
207 .decode(&self.api_secret)
208 .map_err(|e| anyhow::anyhow!("Failed to decode API secret: {e}"))?;
209
210 let nonce_str = nonce.to_string();
211 let sha_input = format!("{nonce_str}{json_body}");
212 let hash = digest::digest(&digest::SHA256, sha_input.as_bytes());
213 let mut message = path.as_bytes().to_vec();
214 message.extend_from_slice(hash.as_ref());
215 let key = hmac::Key::new(hmac::HMAC_SHA512, &secret);
216 let signature = hmac::sign(&key, &message);
217
218 Ok(STANDARD.encode(signature.as_ref()))
219 }
220
221 pub fn sign_futures(&self, path: &str, post_data: &str, nonce: u64) -> anyhow::Result<String> {
234 let secret = STANDARD
235 .decode(&self.api_secret)
236 .map_err(|e| anyhow::anyhow!("Failed to decode API secret: {e}"))?;
237
238 let signing_path = path.strip_prefix("/derivatives").unwrap_or(path);
239 let message = format!("{post_data}{nonce}{signing_path}");
240 let hash = digest::digest(&digest::SHA256, message.as_bytes());
241 let key = hmac::Key::new(hmac::HMAC_SHA512, &secret);
242 let signature = hmac::sign(&key, hash.as_ref());
243
244 Ok(STANDARD.encode(signature.as_ref()))
245 }
246
247 pub fn sign_ws_challenge(&self, challenge: &str) -> anyhow::Result<String> {
254 let secret = STANDARD
255 .decode(&self.api_secret)
256 .map_err(|e| anyhow::anyhow!("Failed to decode API secret: {e}"))?;
257
258 let hash = digest::digest(&digest::SHA256, challenge.as_bytes());
259 let key = hmac::Key::new(hmac::HMAC_SHA512, &secret);
260 let signature = hmac::sign(&key, hash.as_ref());
261
262 Ok(STANDARD.encode(signature.as_ref()))
263 }
264
265 #[must_use]
270 pub fn api_key_masked(&self) -> String {
271 nautilus_core::string::secret::mask_api_key(&self.api_key)
272 }
273}
274
275#[must_use]
280pub fn generate_nonce() -> u64 {
281 let micros = SystemTime::now()
282 .duration_since(UNIX_EPOCH)
283 .expect("System time before UNIX epoch")
284 .as_micros() as u64;
285
286 let counter = NONCE_COUNTER.fetch_add(1, Ordering::Relaxed);
287 micros.wrapping_add(counter)
288}
289
290#[cfg(test)]
291mod tests {
292 use rstest::rstest;
293
294 use super::*;
295
296 #[rstest]
297 fn test_credential_creation() {
298 let cred = KrakenCredential::new("test_key", "test_secret");
299 assert_eq!(cred.api_key(), "test_key");
300 }
301
302 #[rstest]
303 fn test_generate_nonce_uniqueness() {
304 let nonces: Vec<u64> = (0..1000).map(|_| generate_nonce()).collect();
305 let unique: std::collections::HashSet<u64> = nonces.iter().copied().collect();
306 assert_eq!(
307 nonces.len(),
308 unique.len(),
309 "Generated nonces should be unique"
310 );
311 }
312
313 #[rstest]
314 fn test_sign_futures_uses_url_encoded_post_data() {
315 let secret = STANDARD.encode(b"test_secret_key_24bytes!");
319 let cred = KrakenCredential::new("test_key", secret);
320
321 let endpoint = "/derivatives/api/v3/sendorder";
322 let nonce = 1234567890u64;
323
324 let mut params = HashMap::new();
326 params.insert("symbol".to_string(), "PI_XBTUSD".to_string());
327 params.insert("side".to_string(), "buy".to_string());
328 params.insert("orderType".to_string(), "lmt".to_string());
329 params.insert("size".to_string(), "100".to_string());
330 params.insert("limitPrice".to_string(), "50000.5".to_string());
331
332 let post_data = serde_urlencoded::to_string(¶ms).unwrap();
333
334 let signature = cred.sign_futures(endpoint, &post_data, nonce).unwrap();
336
337 assert!(!signature.is_empty());
339 assert!(STANDARD.decode(&signature).is_ok());
340
341 let signature2 = cred.sign_futures(endpoint, &post_data, nonce).unwrap();
343 assert_eq!(signature, signature2);
344
345 let different_post_data = "symbol=PI_ETHUSD&side=sell";
347 let different_sig = cred
348 .sign_futures(endpoint, different_post_data, nonce)
349 .unwrap();
350 assert_ne!(signature, different_sig);
351
352 let different_nonce_sig = cred.sign_futures(endpoint, &post_data, nonce + 1).unwrap();
354 assert_ne!(signature, different_nonce_sig);
355 }
356
357 #[rstest]
358 fn test_sign_futures_strips_derivatives_prefix() {
359 let secret = STANDARD.encode(b"test_secret_key_24bytes!");
361 let cred = KrakenCredential::new("test_key", secret);
362 let nonce = 1234567890u64;
363
364 let with_prefix = cred
366 .sign_futures("/derivatives/api/v3/openpositions", "", nonce)
367 .unwrap();
368 let without_prefix = cred
369 .sign_futures("/api/v3/openpositions", "", nonce)
370 .unwrap();
371
372 assert_eq!(with_prefix, without_prefix);
373 }
374
375 #[rstest]
376 fn test_resolve_spot_with_both_args() {
377 let result =
378 KrakenCredential::resolve_spot(Some("key".to_string()), Some("secret".to_string()));
379 assert!(result.is_some());
380 let cred = result.unwrap();
381 assert_eq!(cred.api_key(), "key");
382 }
383
384 #[rstest]
385 fn test_resolve_spot_with_partial_args() {
386 let (_, secret_var) =
387 credential_env_vars(KrakenProductType::Spot, KrakenEnvironment::Mainnet);
388
389 if std::env::var(secret_var).is_ok() {
391 let result = KrakenCredential::resolve_spot(Some("key".to_string()), None);
392 assert!(result.is_some());
393 assert_eq!(result.unwrap().api_key(), "key");
394 } else {
395 let result = KrakenCredential::resolve_spot(Some("key".to_string()), None);
396 assert!(result.is_none());
397 }
398 }
399
400 #[rstest]
401 fn test_resolve_futures_with_both_args() {
402 let result = KrakenCredential::resolve_futures(
403 Some("key".to_string()),
404 Some("secret".to_string()),
405 false,
406 );
407 assert!(result.is_some());
408 let cred = result.unwrap();
409 assert_eq!(cred.api_key(), "key");
410 }
411
412 #[rstest]
413 fn test_resolve_futures_with_partial_args() {
414 let (_, secret_var) =
415 credential_env_vars(KrakenProductType::Futures, KrakenEnvironment::Mainnet);
416
417 if std::env::var(secret_var).is_ok() {
419 let result = KrakenCredential::resolve_futures(Some("key".to_string()), None, false);
420 assert!(result.is_some());
421 assert_eq!(result.unwrap().api_key(), "key");
422 } else {
423 let result = KrakenCredential::resolve_futures(Some("key".to_string()), None, false);
424 assert!(result.is_none());
425 }
426 }
427
428 #[rstest]
429 fn test_debug_redacts_secret() {
430 let cred = KrakenCredential::new("test_key", "test_secret");
431 let dbg_out = format!("{cred:?}");
432
433 assert!(dbg_out.contains(REDACTED));
434 assert!(!dbg_out.contains("test_secret"));
435 }
436}