nautilus_bitmex/common/
credential.rs1use std::fmt::Debug;
17
18use aws_lc_rs::hmac;
19use ustr::Ustr;
20use zeroize::ZeroizeOnDrop;
21
22#[derive(Clone, ZeroizeOnDrop)]
27pub struct Credential {
28 #[zeroize(skip)]
29 pub api_key: Ustr,
30 api_secret: Box<[u8]>,
31}
32
33impl Debug for Credential {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 f.debug_struct("Credential")
36 .field("api_key", &self.api_key)
37 .field("api_secret", &"<redacted>")
38 .finish()
39 }
40}
41
42impl Credential {
43 #[must_use]
45 pub fn new(api_key: String, api_secret: String) -> Self {
46 let boxed: Box<[u8]> = api_secret.into_bytes().into_boxed_slice();
47
48 Self {
49 api_key: api_key.into(),
50 api_secret: boxed,
51 }
52 }
53
54 #[must_use]
56 pub fn sign(&self, verb: &str, endpoint: &str, expires: i64, data: &str) -> String {
57 let sign_message = format!("{verb}{endpoint}{expires}{data}");
58 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
59 let signature = hmac::sign(&key, sign_message.as_bytes());
60 hex::encode(signature.as_ref())
61 }
62}
63
64#[cfg(test)]
70mod tests {
71 use rstest::rstest;
72
73 use super::*;
74
75 const API_KEY: &str = "LAqUlngMIQkIUjXMUreyu3qn";
76 const API_SECRET: &str = "chNOOS4KvNXR_Xq4k4c9qsfoKWvnDecLATCRlcBwyKDYnWgO";
77
78 #[rstest]
79 fn test_simple_get() {
80 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
81
82 let signature = credential.sign("GET", "/api/v1/instrument", 1518064236, "");
83
84 assert_eq!(
85 signature,
86 "c7682d435d0cfe87c16098df34ef2eb5a549d4c5a3c2b1f0f77b8af73423bf00"
87 );
88 }
89
90 #[rstest]
91 fn test_get_with_query() {
92 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
93
94 let signature = credential.sign(
95 "GET",
96 "/api/v1/instrument?filter=%7B%22symbol%22%3A+%22XBTM15%22%7D",
97 1518064237,
98 "",
99 );
100
101 assert_eq!(
102 signature,
103 "e2f422547eecb5b3cb29ade2127e21b858b235b386bfa45e1c1756eb3383919f"
104 );
105 }
106
107 #[rstest]
108 fn test_post_with_data() {
109 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
110
111 let data = r#"{"symbol":"XBTM15","price":219.0,"clOrdID":"mm_bitmex_1a/oemUeQ4CAJZgP3fjHsA","orderQty":98}"#;
112
113 let signature = credential.sign("POST", "/api/v1/order", 1518064238, data);
114
115 assert_eq!(
116 signature,
117 "1749cd2ccae4aa49048ae09f0b95110cee706e0944e6a14ad0b3a8cb45bd336b"
118 );
119 }
120
121 #[rstest]
122 fn test_debug_redacts_secret() {
123 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
124 let dbg_out = format!("{credential:?}");
125 assert!(dbg_out.contains("api_secret: \"<redacted>\""));
126 assert!(!dbg_out.contains("chNOO"));
127 let secret_bytes_dbg = format!("{:?}", API_SECRET.as_bytes());
128 assert!(
129 !dbg_out.contains(&secret_bytes_dbg),
130 "Debug output must not contain raw secret bytes"
131 );
132 }
133}