1use base64::{Engine as _, engine::general_purpose};
11use ccxt_core::credentials::SecretString;
12use hmac::{Hmac, Mac};
13use reqwest::header::{HeaderMap, HeaderValue};
14use sha2::Sha256;
15
16#[derive(Debug, Clone)]
23pub struct OkxAuth {
24 api_key: SecretString,
26 secret: SecretString,
28 passphrase: SecretString,
30}
31
32impl OkxAuth {
33 pub fn new(api_key: String, secret: String, passphrase: String) -> Self {
57 Self {
58 api_key: SecretString::new(api_key),
59 secret: SecretString::new(secret),
60 passphrase: SecretString::new(passphrase),
61 }
62 }
63
64 pub fn api_key(&self) -> &str {
66 self.api_key.expose_secret()
67 }
68
69 pub fn passphrase(&self) -> &str {
71 self.passphrase.expose_secret()
72 }
73
74 pub fn build_sign_string(
89 &self,
90 timestamp: &str,
91 method: &str,
92 path: &str,
93 body: &str,
94 ) -> String {
95 format!("{}{}{}{}", timestamp, method.to_uppercase(), path, body)
96 }
97
98 pub fn sign(&self, timestamp: &str, method: &str, path: &str, body: &str) -> String {
126 let sign_string = self.build_sign_string(timestamp, method, path, body);
127 self.hmac_sha256_base64(&sign_string)
128 }
129
130 fn hmac_sha256_base64(&self, message: &str) -> String {
132 type HmacSha256 = Hmac<Sha256>;
133
134 let mut mac = HmacSha256::new_from_slice(self.secret.expose_secret_bytes())
136 .expect("HMAC-SHA256 accepts keys of any length; this is an infallible operation");
137 mac.update(message.as_bytes());
138 let result = mac.finalize().into_bytes();
139
140 general_purpose::STANDARD.encode(result)
141 }
142
143 pub fn add_auth_headers(&self, headers: &mut HeaderMap, timestamp: &str, sign: &str) {
180 headers.insert(
181 "OK-ACCESS-KEY",
182 HeaderValue::from_str(self.api_key.expose_secret())
183 .unwrap_or_else(|_| HeaderValue::from_static("")),
184 );
185 headers.insert(
186 "OK-ACCESS-SIGN",
187 HeaderValue::from_str(sign).unwrap_or_else(|_| HeaderValue::from_static("")),
188 );
189 headers.insert(
190 "OK-ACCESS-TIMESTAMP",
191 HeaderValue::from_str(timestamp).unwrap_or_else(|_| HeaderValue::from_static("")),
192 );
193 headers.insert(
194 "OK-ACCESS-PASSPHRASE",
195 HeaderValue::from_str(self.passphrase.expose_secret())
196 .unwrap_or_else(|_| HeaderValue::from_static("")),
197 );
198 }
199
200 pub fn create_auth_headers(
215 &self,
216 timestamp: &str,
217 method: &str,
218 path: &str,
219 body: &str,
220 ) -> HeaderMap {
221 let sign = self.sign(timestamp, method, path, body);
222 let mut headers = HeaderMap::new();
223 self.add_auth_headers(&mut headers, timestamp, &sign);
224 headers
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_auth_new() {
234 let auth = OkxAuth::new(
235 "test-api-key".to_string(),
236 "test-secret".to_string(),
237 "test-passphrase".to_string(),
238 );
239
240 assert_eq!(auth.api_key(), "test-api-key");
241 assert_eq!(auth.passphrase(), "test-passphrase");
242 }
243
244 #[test]
245 fn test_build_sign_string() {
246 let auth = OkxAuth::new(
247 "api-key".to_string(),
248 "secret".to_string(),
249 "passphrase".to_string(),
250 );
251
252 let sign_string = auth.build_sign_string(
253 "2021-01-01T00:00:00.000Z",
254 "GET",
255 "/api/v5/account/balance",
256 "",
257 );
258 assert_eq!(
259 sign_string,
260 "2021-01-01T00:00:00.000ZGET/api/v5/account/balance"
261 );
262
263 let sign_string_post = auth.build_sign_string(
264 "2021-01-01T00:00:00.000Z",
265 "POST",
266 "/api/v5/trade/order",
267 r#"{"instId":"BTC-USDT","side":"buy"}"#,
268 );
269 assert_eq!(
270 sign_string_post,
271 r#"2021-01-01T00:00:00.000ZPOST/api/v5/trade/order{"instId":"BTC-USDT","side":"buy"}"#
272 );
273 }
274
275 #[test]
276 fn test_sign_deterministic() {
277 let auth = OkxAuth::new(
278 "api-key".to_string(),
279 "test-secret-key".to_string(),
280 "passphrase".to_string(),
281 );
282
283 let sig1 = auth.sign(
284 "2021-01-01T00:00:00.000Z",
285 "GET",
286 "/api/v5/account/balance",
287 "",
288 );
289 let sig2 = auth.sign(
290 "2021-01-01T00:00:00.000Z",
291 "GET",
292 "/api/v5/account/balance",
293 "",
294 );
295
296 assert_eq!(sig1, sig2);
297 assert!(!sig1.is_empty());
298 }
299
300 #[test]
301 fn test_sign_different_inputs() {
302 let auth = OkxAuth::new(
303 "api-key".to_string(),
304 "test-secret-key".to_string(),
305 "passphrase".to_string(),
306 );
307
308 let sig1 = auth.sign(
309 "2021-01-01T00:00:00.000Z",
310 "GET",
311 "/api/v5/account/balance",
312 "",
313 );
314 let sig2 = auth.sign(
315 "2021-01-01T00:00:01.000Z",
316 "GET",
317 "/api/v5/account/balance",
318 "",
319 );
320 let sig3 = auth.sign(
321 "2021-01-01T00:00:00.000Z",
322 "POST",
323 "/api/v5/account/balance",
324 "",
325 );
326
327 assert_ne!(sig1, sig2);
328 assert_ne!(sig1, sig3);
329 }
330
331 #[test]
332 fn test_add_auth_headers() {
333 let auth = OkxAuth::new(
334 "test-api-key".to_string(),
335 "test-secret".to_string(),
336 "test-passphrase".to_string(),
337 );
338
339 let mut headers = HeaderMap::new();
340 let timestamp = "2021-01-01T00:00:00.000Z";
341 let signature = auth.sign(timestamp, "GET", "/api/v5/account/balance", "");
342 auth.add_auth_headers(&mut headers, timestamp, &signature);
343
344 assert_eq!(headers.get("OK-ACCESS-KEY").unwrap(), "test-api-key");
345 assert_eq!(headers.get("OK-ACCESS-SIGN").unwrap(), &signature);
346 assert_eq!(
347 headers.get("OK-ACCESS-TIMESTAMP").unwrap(),
348 "2021-01-01T00:00:00.000Z"
349 );
350 assert_eq!(
351 headers.get("OK-ACCESS-PASSPHRASE").unwrap(),
352 "test-passphrase"
353 );
354 }
355
356 #[test]
357 fn test_create_auth_headers() {
358 let auth = OkxAuth::new(
359 "test-api-key".to_string(),
360 "test-secret".to_string(),
361 "test-passphrase".to_string(),
362 );
363
364 let headers = auth.create_auth_headers(
365 "2021-01-01T00:00:00.000Z",
366 "GET",
367 "/api/v5/account/balance",
368 "",
369 );
370
371 assert!(headers.contains_key("OK-ACCESS-KEY"));
372 assert!(headers.contains_key("OK-ACCESS-SIGN"));
373 assert!(headers.contains_key("OK-ACCESS-TIMESTAMP"));
374 assert!(headers.contains_key("OK-ACCESS-PASSPHRASE"));
375 }
376
377 #[test]
378 fn test_method_case_insensitive() {
379 let auth = OkxAuth::new(
380 "api-key".to_string(),
381 "test-secret".to_string(),
382 "passphrase".to_string(),
383 );
384
385 let sign_string_lower = auth.build_sign_string(
387 "2021-01-01T00:00:00.000Z",
388 "get",
389 "/api/v5/account/balance",
390 "",
391 );
392 let sign_string_upper = auth.build_sign_string(
393 "2021-01-01T00:00:00.000Z",
394 "GET",
395 "/api/v5/account/balance",
396 "",
397 );
398
399 assert_eq!(sign_string_lower, sign_string_upper);
400 }
401
402 #[test]
403 fn test_signature_is_base64() {
404 let auth = OkxAuth::new(
405 "api-key".to_string(),
406 "test-secret".to_string(),
407 "passphrase".to_string(),
408 );
409
410 let signature = auth.sign(
411 "2021-01-01T00:00:00.000Z",
412 "GET",
413 "/api/v5/account/balance",
414 "",
415 );
416
417 assert!(
419 signature
420 .chars()
421 .all(|c| c.is_alphanumeric() || c == '+' || c == '/' || c == '=')
422 );
423
424 let decoded = general_purpose::STANDARD.decode(&signature);
426 assert!(decoded.is_ok());
427
428 assert_eq!(decoded.unwrap().len(), 32);
430 }
431
432 #[test]
435 fn test_signature_with_known_inputs() {
436 let auth = OkxAuth::new(
437 "okx_api_key".to_string(),
438 "okx-secret-key".to_string(),
439 "my-passphrase".to_string(),
440 );
441
442 let timestamp = "2021-01-01T00:00:00.000Z";
444 let method = "GET";
445 let path = "/api/v5/account/balance";
446 let body = "";
447
448 let signature = auth.sign(timestamp, method, path, body);
450
451 let expected_sign_string = "2021-01-01T00:00:00.000ZGET/api/v5/account/balance";
453 assert_eq!(
454 auth.build_sign_string(timestamp, method, path, body),
455 expected_sign_string
456 );
457
458 assert!(!signature.is_empty());
460 let decoded = general_purpose::STANDARD.decode(&signature);
461 assert!(decoded.is_ok());
462 assert_eq!(decoded.unwrap().len(), 32);
463
464 let signature2 = auth.sign(timestamp, method, path, body);
466 assert_eq!(signature, signature2);
467 }
468
469 #[test]
471 fn test_signature_with_post_body() {
472 let auth = OkxAuth::new(
473 "okx_api_key".to_string(),
474 "okx-secret-key".to_string(),
475 "my-passphrase".to_string(),
476 );
477
478 let timestamp = "2021-01-01T00:00:00.000Z";
479 let method = "POST";
480 let path = "/api/v5/trade/order";
481 let body = r#"{"instId":"BTC-USDT","tdMode":"cash","side":"buy","ordType":"limit","px":"50000","sz":"0.001"}"#;
482
483 let signature = auth.sign(timestamp, method, path, body);
484
485 let expected_sign_string = format!("{}{}{}{}", timestamp, method, path, body);
487 assert_eq!(
488 auth.build_sign_string(timestamp, method, path, body),
489 expected_sign_string
490 );
491
492 assert!(!signature.is_empty());
494 let decoded = general_purpose::STANDARD.decode(&signature);
495 assert!(decoded.is_ok());
496 assert_eq!(decoded.unwrap().len(), 32);
497 }
498
499 #[test]
501 fn test_header_values_correctness() {
502 let api_key = "my-api-key-12345";
503 let secret = "my-secret-key-67890";
504 let passphrase = "my-passphrase-abc";
505
506 let auth = OkxAuth::new(
507 api_key.to_string(),
508 secret.to_string(),
509 passphrase.to_string(),
510 );
511
512 let timestamp = "2021-01-01T00:00:00.000Z";
513 let method = "GET";
514 let path = "/api/v5/account/balance";
515 let body = "";
516
517 let headers = auth.create_auth_headers(timestamp, method, path, body);
518
519 assert_eq!(
521 headers.get("OK-ACCESS-KEY").unwrap().to_str().unwrap(),
522 api_key
523 );
524
525 assert_eq!(
527 headers
528 .get("OK-ACCESS-TIMESTAMP")
529 .unwrap()
530 .to_str()
531 .unwrap(),
532 timestamp
533 );
534
535 assert_eq!(
537 headers
538 .get("OK-ACCESS-PASSPHRASE")
539 .unwrap()
540 .to_str()
541 .unwrap(),
542 passphrase
543 );
544
545 let sign_header = headers.get("OK-ACCESS-SIGN").unwrap().to_str().unwrap();
547 assert!(!sign_header.is_empty());
548 let decoded = general_purpose::STANDARD.decode(sign_header);
549 assert!(decoded.is_ok());
550 }
551
552 #[test]
554 fn test_signature_with_query_params() {
555 let auth = OkxAuth::new(
556 "api-key".to_string(),
557 "secret-key".to_string(),
558 "passphrase".to_string(),
559 );
560
561 let timestamp = "2021-01-01T00:00:00.000Z";
562 let method = "GET";
563 let path = "/api/v5/market/ticker?instId=BTC-USDT";
564 let body = "";
565
566 let signature = auth.sign(timestamp, method, path, body);
567
568 let sign_string = auth.build_sign_string(timestamp, method, path, body);
570 assert!(sign_string.contains("?instId=BTC-USDT"));
571
572 assert!(!signature.is_empty());
574 let decoded = general_purpose::STANDARD.decode(&signature);
575 assert!(decoded.is_ok());
576 }
577}