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 serde_urlencoded;
28use zeroize::{Zeroize, ZeroizeOnDrop};
29
30static NONCE_COUNTER: AtomicU64 = AtomicU64::new(0);
32
33#[derive(Clone, Zeroize, ZeroizeOnDrop)]
35pub struct KrakenCredential {
36 api_key: String,
37 api_secret: String,
38}
39
40impl Debug for KrakenCredential {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct(stringify!(KrakenCredential))
43 .field("api_key", &self.api_key)
44 .field("api_secret", &"<redacted>")
45 .finish()
46 }
47}
48
49impl KrakenCredential {
50 pub fn new(api_key: impl Into<String>, api_secret: impl Into<String>) -> Self {
52 Self {
53 api_key: api_key.into(),
54 api_secret: api_secret.into(),
55 }
56 }
57
58 #[must_use]
66 pub fn from_env_spot() -> Option<Self> {
67 let key = std::env::var("KRAKEN_SPOT_API_KEY").ok()?;
68 let secret = std::env::var("KRAKEN_SPOT_API_SECRET").ok()?;
69
70 Some(Self::new(key, secret))
71 }
72
73 #[must_use]
80 pub fn from_env_futures(demo: bool) -> Option<Self> {
81 let (key_var, secret_var) = if demo {
82 (
83 "KRAKEN_FUTURES_DEMO_API_KEY",
84 "KRAKEN_FUTURES_DEMO_API_SECRET",
85 )
86 } else {
87 ("KRAKEN_FUTURES_API_KEY", "KRAKEN_FUTURES_API_SECRET")
88 };
89
90 let key = std::env::var(key_var).ok()?;
91 let secret = std::env::var(secret_var).ok()?;
92
93 Some(Self::new(key, secret))
94 }
95
96 #[must_use]
101 pub fn resolve_spot(api_key: Option<String>, api_secret: Option<String>) -> Option<Self> {
102 match (api_key, api_secret) {
103 (Some(k), Some(s)) => Some(Self::new(k, s)),
104 _ => Self::from_env_spot(),
105 }
106 }
107
108 #[must_use]
113 pub fn resolve_futures(
114 api_key: Option<String>,
115 api_secret: Option<String>,
116 demo: bool,
117 ) -> Option<Self> {
118 match (api_key, api_secret) {
119 (Some(k), Some(s)) => Some(Self::new(k, s)),
120 _ => Self::from_env_futures(demo),
121 }
122 }
123
124 pub fn api_key(&self) -> &str {
126 &self.api_key
127 }
128
129 pub fn into_parts(&self) -> (String, String) {
131 (self.api_key.clone(), self.api_secret.clone())
132 }
133
134 pub fn sign_spot(
143 &self,
144 path: &str,
145 nonce: u64,
146 params: &HashMap<String, String>,
147 ) -> anyhow::Result<(String, String)> {
148 let secret = STANDARD
149 .decode(&self.api_secret)
150 .map_err(|e| anyhow::anyhow!("Failed to decode API secret: {e}"))?;
151
152 let nonce_str = nonce.to_string();
153 let mut post_data = format!("nonce={nonce_str}");
154 if !params.is_empty() {
155 let encoded = serde_urlencoded::to_string(params)
156 .map_err(|e| anyhow::anyhow!("Failed to encode params: {e}"))?;
157 post_data.push('&');
158 post_data.push_str(&encoded);
159 }
160
161 let sha_input = format!("{nonce_str}{post_data}");
162 let hash = digest::digest(&digest::SHA256, sha_input.as_bytes());
163 let mut message = path.as_bytes().to_vec();
164 message.extend_from_slice(hash.as_ref());
165 let key = hmac::Key::new(hmac::HMAC_SHA512, &secret);
166 let signature = hmac::sign(&key, &message);
167
168 Ok((STANDARD.encode(signature.as_ref()), post_data))
169 }
170
171 pub fn sign_spot_json(
176 &self,
177 path: &str,
178 nonce: u64,
179 json_body: &str,
180 ) -> anyhow::Result<String> {
181 let secret = STANDARD
182 .decode(&self.api_secret)
183 .map_err(|e| anyhow::anyhow!("Failed to decode API secret: {e}"))?;
184
185 let nonce_str = nonce.to_string();
186 let sha_input = format!("{nonce_str}{json_body}");
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()))
194 }
195
196 pub fn sign_futures(&self, path: &str, post_data: &str, nonce: u64) -> anyhow::Result<String> {
209 let secret = STANDARD
210 .decode(&self.api_secret)
211 .map_err(|e| anyhow::anyhow!("Failed to decode API secret: {e}"))?;
212
213 let signing_path = path.strip_prefix("/derivatives").unwrap_or(path);
214 let message = format!("{post_data}{nonce}{signing_path}");
215 let hash = digest::digest(&digest::SHA256, message.as_bytes());
216 let key = hmac::Key::new(hmac::HMAC_SHA512, &secret);
217 let signature = hmac::sign(&key, hash.as_ref());
218
219 Ok(STANDARD.encode(signature.as_ref()))
220 }
221
222 pub fn sign_ws_challenge(&self, challenge: &str) -> anyhow::Result<String> {
229 let secret = STANDARD
230 .decode(&self.api_secret)
231 .map_err(|e| anyhow::anyhow!("Failed to decode API secret: {e}"))?;
232
233 let hash = digest::digest(&digest::SHA256, challenge.as_bytes());
234 let key = hmac::Key::new(hmac::HMAC_SHA512, &secret);
235 let signature = hmac::sign(&key, hash.as_ref());
236
237 Ok(STANDARD.encode(signature.as_ref()))
238 }
239
240 #[must_use]
245 pub fn api_key_masked(&self) -> String {
246 nautilus_core::string::mask_api_key(&self.api_key)
247 }
248}
249
250#[must_use]
255pub fn generate_nonce() -> u64 {
256 let micros = SystemTime::now()
257 .duration_since(UNIX_EPOCH)
258 .expect("System time before UNIX epoch")
259 .as_micros() as u64;
260
261 let counter = NONCE_COUNTER.fetch_add(1, Ordering::Relaxed);
262 micros.wrapping_add(counter)
263}
264
265#[cfg(test)]
266mod tests {
267 use rstest::rstest;
268
269 use super::*;
270
271 #[rstest]
272 fn test_credential_creation() {
273 let cred = KrakenCredential::new("test_key", "test_secret");
274 assert_eq!(cred.api_key(), "test_key");
275 }
276
277 #[rstest]
278 fn test_generate_nonce_uniqueness() {
279 let nonces: Vec<u64> = (0..1000).map(|_| generate_nonce()).collect();
280 let unique: std::collections::HashSet<u64> = nonces.iter().copied().collect();
281 assert_eq!(
282 nonces.len(),
283 unique.len(),
284 "Generated nonces should be unique"
285 );
286 }
287
288 #[rstest]
289 fn test_sign_futures_uses_url_encoded_post_data() {
290 let secret = STANDARD.encode(b"test_secret_key_24bytes!");
294 let cred = KrakenCredential::new("test_key", secret);
295
296 let endpoint = "/derivatives/api/v3/sendorder";
297 let nonce = 1234567890u64;
298
299 let mut params = HashMap::new();
301 params.insert("symbol".to_string(), "PI_XBTUSD".to_string());
302 params.insert("side".to_string(), "buy".to_string());
303 params.insert("orderType".to_string(), "lmt".to_string());
304 params.insert("size".to_string(), "100".to_string());
305 params.insert("limitPrice".to_string(), "50000.5".to_string());
306
307 let post_data = serde_urlencoded::to_string(¶ms).unwrap();
308
309 let signature = cred.sign_futures(endpoint, &post_data, nonce).unwrap();
311
312 assert!(!signature.is_empty());
314 assert!(STANDARD.decode(&signature).is_ok());
315
316 let signature2 = cred.sign_futures(endpoint, &post_data, nonce).unwrap();
318 assert_eq!(signature, signature2);
319
320 let different_post_data = "symbol=PI_ETHUSD&side=sell";
322 let different_sig = cred
323 .sign_futures(endpoint, different_post_data, nonce)
324 .unwrap();
325 assert_ne!(signature, different_sig);
326
327 let different_nonce_sig = cred.sign_futures(endpoint, &post_data, nonce + 1).unwrap();
329 assert_ne!(signature, different_nonce_sig);
330 }
331
332 #[rstest]
333 fn test_sign_futures_strips_derivatives_prefix() {
334 let secret = STANDARD.encode(b"test_secret_key_24bytes!");
336 let cred = KrakenCredential::new("test_key", secret);
337 let nonce = 1234567890u64;
338
339 let with_prefix = cred
341 .sign_futures("/derivatives/api/v3/openpositions", "", nonce)
342 .unwrap();
343 let without_prefix = cred
344 .sign_futures("/api/v3/openpositions", "", nonce)
345 .unwrap();
346
347 assert_eq!(with_prefix, without_prefix);
348 }
349
350 #[rstest]
351 fn test_resolve_spot_with_both_args() {
352 let result =
353 KrakenCredential::resolve_spot(Some("key".to_string()), Some("secret".to_string()));
354 assert!(result.is_some());
355 let cred = result.unwrap();
356 assert_eq!(cred.api_key(), "key");
357 }
358
359 #[rstest]
360 fn test_resolve_spot_with_partial_args_falls_back_to_env() {
361 let result = KrakenCredential::resolve_spot(Some("key".to_string()), None);
364
365 if let Some(cred) = result {
367 assert_ne!(cred.api_key(), "key");
368 }
369 }
370
371 #[rstest]
372 fn test_resolve_futures_with_both_args() {
373 let result = KrakenCredential::resolve_futures(
374 Some("key".to_string()),
375 Some("secret".to_string()),
376 false,
377 );
378 assert!(result.is_some());
379 let cred = result.unwrap();
380 assert_eq!(cred.api_key(), "key");
381 }
382
383 #[rstest]
384 fn test_resolve_futures_with_partial_args_falls_back_to_env() {
385 let result = KrakenCredential::resolve_futures(Some("key".to_string()), None, false);
387
388 if let Some(cred) = result {
390 assert_ne!(cred.api_key(), "key");
391 }
392 }
393}