nautilus_bitmex/common/
credential.rs1#![allow(unused_assignments)] use std::fmt::Debug;
21
22use aws_lc_rs::hmac;
23use nautilus_core::string::mask_api_key;
24use ustr::Ustr;
25use zeroize::ZeroizeOnDrop;
26
27#[derive(Clone, ZeroizeOnDrop)]
32pub struct Credential {
33 #[zeroize(skip)]
34 pub api_key: Ustr,
35 api_secret: Box<[u8]>,
36}
37
38impl Debug for Credential {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.debug_struct(stringify!(Credential))
41 .field("api_key", &self.api_key)
42 .field("api_secret", &"<redacted>")
43 .finish()
44 }
45}
46
47impl Credential {
48 #[must_use]
50 pub fn new(api_key: String, api_secret: String) -> Self {
51 let boxed: Box<[u8]> = api_secret.into_bytes().into_boxed_slice();
52
53 Self {
54 api_key: api_key.into(),
55 api_secret: boxed,
56 }
57 }
58
59 #[must_use]
61 pub fn sign(&self, verb: &str, endpoint: &str, expires: i64, data: &str) -> String {
62 let sign_message = format!("{verb}{endpoint}{expires}{data}");
63 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
64 let signature = hmac::sign(&key, sign_message.as_bytes());
65 hex::encode(signature.as_ref())
66 }
67
68 #[must_use]
73 pub fn api_key_masked(&self) -> String {
74 mask_api_key(self.api_key.as_str())
75 }
76}
77
78#[cfg(test)]
80mod tests {
81 use rstest::rstest;
82
83 use super::*;
84 use crate::common::testing::load_test_json;
85
86 const API_KEY: &str = "LAqUlngMIQkIUjXMUreyu3qn";
87 const API_SECRET: &str = "chNOOS4KvNXR_Xq4k4c9qsfoKWvnDecLATCRlcBwyKDYnWgO";
88
89 #[rstest]
90 fn test_simple_get() {
91 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
92
93 let signature = credential.sign("GET", "/api/v1/instrument", 1518064236, "");
94
95 assert_eq!(
96 signature,
97 "c7682d435d0cfe87c16098df34ef2eb5a549d4c5a3c2b1f0f77b8af73423bf00"
98 );
99 }
100
101 #[rstest]
102 fn test_get_with_query() {
103 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
104
105 let signature = credential.sign(
106 "GET",
107 "/api/v1/instrument?filter=%7B%22symbol%22%3A+%22XBTM15%22%7D",
108 1518064237,
109 "",
110 );
111
112 assert_eq!(
113 signature,
114 "e2f422547eecb5b3cb29ade2127e21b858b235b386bfa45e1c1756eb3383919f"
115 );
116 }
117
118 #[rstest]
119 fn test_post_with_data() {
120 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
121
122 let data = load_test_json("credential_post_order.json");
123
124 let signature = credential.sign("POST", "/api/v1/order", 1518064238, data.trim_end());
125
126 assert_eq!(
127 signature,
128 "1749cd2ccae4aa49048ae09f0b95110cee706e0944e6a14ad0b3a8cb45bd336b"
129 );
130 }
131
132 #[rstest]
133 fn test_debug_redacts_secret() {
134 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
135 let dbg_out = format!("{credential:?}");
136 assert!(dbg_out.contains("api_secret: \"<redacted>\""));
137 assert!(!dbg_out.contains("chNOO"));
138 let secret_bytes_dbg = format!("{:?}", API_SECRET.as_bytes());
139 assert!(
140 !dbg_out.contains(&secret_bytes_dbg),
141 "Debug output must not contain raw secret bytes"
142 );
143 }
144}