1use std::collections::HashMap;
19
20use aws_lc_rs::{digest, hmac};
21use base64::{Engine, engine::general_purpose::STANDARD};
22use serde_urlencoded;
23use zeroize::{Zeroize, ZeroizeOnDrop};
24
25#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
27pub struct KrakenCredential {
28 api_key: String,
29 api_secret: String,
30}
31
32impl KrakenCredential {
33 pub fn new(api_key: impl Into<String>, api_secret: impl Into<String>) -> Self {
35 Self {
36 api_key: api_key.into(),
37 api_secret: api_secret.into(),
38 }
39 }
40
41 #[must_use]
49 pub fn from_env_spot() -> Option<Self> {
50 let key = std::env::var("KRAKEN_SPOT_API_KEY").ok()?;
51 let secret = std::env::var("KRAKEN_SPOT_API_SECRET").ok()?;
52
53 Some(Self::new(key, secret))
54 }
55
56 #[must_use]
63 pub fn from_env_futures(demo: bool) -> Option<Self> {
64 let (key_var, secret_var) = if demo {
65 (
66 "KRAKEN_FUTURES_DEMO_API_KEY",
67 "KRAKEN_FUTURES_DEMO_API_SECRET",
68 )
69 } else {
70 ("KRAKEN_FUTURES_API_KEY", "KRAKEN_FUTURES_API_SECRET")
71 };
72
73 let key = std::env::var(key_var).ok()?;
74 let secret = std::env::var(secret_var).ok()?;
75
76 Some(Self::new(key, secret))
77 }
78
79 #[must_use]
84 pub fn resolve_spot(api_key: Option<String>, api_secret: Option<String>) -> Option<Self> {
85 match (api_key, api_secret) {
86 (Some(k), Some(s)) => Some(Self::new(k, s)),
87 _ => Self::from_env_spot(),
88 }
89 }
90
91 #[must_use]
96 pub fn resolve_futures(
97 api_key: Option<String>,
98 api_secret: Option<String>,
99 demo: bool,
100 ) -> Option<Self> {
101 match (api_key, api_secret) {
102 (Some(k), Some(s)) => Some(Self::new(k, s)),
103 _ => Self::from_env_futures(demo),
104 }
105 }
106
107 pub fn api_key(&self) -> &str {
109 &self.api_key
110 }
111
112 pub fn into_parts(&self) -> (String, String) {
114 (self.api_key.clone(), self.api_secret.clone())
115 }
116
117 pub fn sign_spot(
126 &self,
127 path: &str,
128 nonce: u64,
129 params: &HashMap<String, String>,
130 ) -> anyhow::Result<(String, String)> {
131 let secret = STANDARD
132 .decode(&self.api_secret)
133 .map_err(|e| anyhow::anyhow!("Failed to decode API secret: {e}"))?;
134
135 let nonce_str = nonce.to_string();
136 let mut post_data = format!("nonce={nonce_str}");
137 if !params.is_empty() {
138 let encoded = serde_urlencoded::to_string(params)
139 .map_err(|e| anyhow::anyhow!("Failed to encode params: {e}"))?;
140 post_data.push('&');
141 post_data.push_str(&encoded);
142 }
143
144 let sha_input = format!("{nonce_str}{post_data}");
145 let hash = digest::digest(&digest::SHA256, sha_input.as_bytes());
146 let mut message = path.as_bytes().to_vec();
147 message.extend_from_slice(hash.as_ref());
148 let key = hmac::Key::new(hmac::HMAC_SHA512, &secret);
149 let signature = hmac::sign(&key, &message);
150
151 Ok((STANDARD.encode(signature.as_ref()), post_data))
152 }
153
154 pub fn sign_spot_json(
159 &self,
160 path: &str,
161 nonce: u64,
162 json_body: &str,
163 ) -> anyhow::Result<String> {
164 let secret = STANDARD
165 .decode(&self.api_secret)
166 .map_err(|e| anyhow::anyhow!("Failed to decode API secret: {e}"))?;
167
168 let nonce_str = nonce.to_string();
169 let sha_input = format!("{nonce_str}{json_body}");
170 let hash = digest::digest(&digest::SHA256, sha_input.as_bytes());
171 let mut message = path.as_bytes().to_vec();
172 message.extend_from_slice(hash.as_ref());
173 let key = hmac::Key::new(hmac::HMAC_SHA512, &secret);
174 let signature = hmac::sign(&key, &message);
175
176 Ok(STANDARD.encode(signature.as_ref()))
177 }
178
179 pub fn sign_futures(&self, path: &str, post_data: &str, nonce: u64) -> anyhow::Result<String> {
192 let secret = STANDARD
193 .decode(&self.api_secret)
194 .map_err(|e| anyhow::anyhow!("Failed to decode API secret: {e}"))?;
195
196 let signing_path = path.strip_prefix("/derivatives").unwrap_or(path);
197 let message = format!("{post_data}{nonce}{signing_path}");
198 let hash = digest::digest(&digest::SHA256, message.as_bytes());
199 let key = hmac::Key::new(hmac::HMAC_SHA512, &secret);
200 let signature = hmac::sign(&key, hash.as_ref());
201
202 Ok(STANDARD.encode(signature.as_ref()))
203 }
204
205 pub fn sign_ws_challenge(&self, challenge: &str) -> anyhow::Result<String> {
212 let secret = STANDARD
213 .decode(&self.api_secret)
214 .map_err(|e| anyhow::anyhow!("Failed to decode API secret: {e}"))?;
215
216 let hash = digest::digest(&digest::SHA256, challenge.as_bytes());
217 let key = hmac::Key::new(hmac::HMAC_SHA512, &secret);
218 let signature = hmac::sign(&key, hash.as_ref());
219
220 Ok(STANDARD.encode(signature.as_ref()))
221 }
222
223 #[must_use]
228 pub fn api_key_masked(&self) -> String {
229 nautilus_core::string::mask_api_key(&self.api_key)
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use rstest::rstest;
236
237 use super::*;
238
239 #[rstest]
240 fn test_credential_creation() {
241 let cred = KrakenCredential::new("test_key", "test_secret");
242 assert_eq!(cred.api_key(), "test_key");
243 }
244
245 #[rstest]
246 fn test_sign_futures_uses_url_encoded_post_data() {
247 let secret = STANDARD.encode(b"test_secret_key_24bytes!");
251 let cred = KrakenCredential::new("test_key", secret);
252
253 let endpoint = "/derivatives/api/v3/sendorder";
254 let nonce = 1234567890u64;
255
256 let mut params = HashMap::new();
258 params.insert("symbol".to_string(), "PI_XBTUSD".to_string());
259 params.insert("side".to_string(), "buy".to_string());
260 params.insert("orderType".to_string(), "lmt".to_string());
261 params.insert("size".to_string(), "100".to_string());
262 params.insert("limitPrice".to_string(), "50000.5".to_string());
263
264 let post_data = serde_urlencoded::to_string(¶ms).unwrap();
265
266 let signature = cred.sign_futures(endpoint, &post_data, nonce).unwrap();
268
269 assert!(!signature.is_empty());
271 assert!(STANDARD.decode(&signature).is_ok());
272
273 let signature2 = cred.sign_futures(endpoint, &post_data, nonce).unwrap();
275 assert_eq!(signature, signature2);
276
277 let different_post_data = "symbol=PI_ETHUSD&side=sell";
279 let different_sig = cred
280 .sign_futures(endpoint, different_post_data, nonce)
281 .unwrap();
282 assert_ne!(signature, different_sig);
283
284 let different_nonce_sig = cred.sign_futures(endpoint, &post_data, nonce + 1).unwrap();
286 assert_ne!(signature, different_nonce_sig);
287 }
288
289 #[rstest]
290 fn test_sign_futures_strips_derivatives_prefix() {
291 let secret = STANDARD.encode(b"test_secret_key_24bytes!");
293 let cred = KrakenCredential::new("test_key", secret);
294 let nonce = 1234567890u64;
295
296 let with_prefix = cred
298 .sign_futures("/derivatives/api/v3/openpositions", "", nonce)
299 .unwrap();
300 let without_prefix = cred
301 .sign_futures("/api/v3/openpositions", "", nonce)
302 .unwrap();
303
304 assert_eq!(with_prefix, without_prefix);
305 }
306
307 #[rstest]
308 fn test_resolve_spot_with_both_args() {
309 let result =
310 KrakenCredential::resolve_spot(Some("key".to_string()), Some("secret".to_string()));
311 assert!(result.is_some());
312 let cred = result.unwrap();
313 assert_eq!(cred.api_key(), "key");
314 }
315
316 #[rstest]
317 fn test_resolve_spot_with_partial_args_falls_back_to_env() {
318 let result = KrakenCredential::resolve_spot(Some("key".to_string()), None);
321
322 if let Some(cred) = result {
324 assert_ne!(cred.api_key(), "key");
325 }
326 }
327
328 #[rstest]
329 fn test_resolve_futures_with_both_args() {
330 let result = KrakenCredential::resolve_futures(
331 Some("key".to_string()),
332 Some("secret".to_string()),
333 false,
334 );
335 assert!(result.is_some());
336 let cred = result.unwrap();
337 assert_eq!(cred.api_key(), "key");
338 }
339
340 #[rstest]
341 fn test_resolve_futures_with_partial_args_falls_back_to_env() {
342 let result = KrakenCredential::resolve_futures(Some("key".to_string()), None, false);
344
345 if let Some(cred) = result {
347 assert_ne!(cred.api_key(), "key");
348 }
349 }
350}