nautilus_bitmex/common/
credential.rs1#![allow(unused_assignments)] use std::fmt::Debug;
21
22use aws_lc_rs::hmac;
23use nautilus_core::{
24 env::resolve_env_var_pair,
25 hex,
26 string::secret::{REDACTED, mask_api_key},
27};
28use zeroize::ZeroizeOnDrop;
29
30use crate::common::enums::BitmexEnvironment;
31
32#[must_use]
35pub fn credential_env_vars(environment: BitmexEnvironment) -> (&'static str, &'static str) {
36 match environment {
37 BitmexEnvironment::Testnet => ("BITMEX_TESTNET_API_KEY", "BITMEX_TESTNET_API_SECRET"),
38 BitmexEnvironment::Mainnet => ("BITMEX_API_KEY", "BITMEX_API_SECRET"),
39 }
40}
41
42#[derive(Clone, ZeroizeOnDrop)]
47pub struct Credential {
48 api_key: Box<str>,
49 api_secret: Box<[u8]>,
50}
51
52impl Debug for Credential {
53 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54 f.debug_struct(stringify!(Credential))
55 .field("api_key", &self.api_key)
56 .field("api_secret", &REDACTED)
57 .finish()
58 }
59}
60
61impl Credential {
62 #[must_use]
64 pub fn new(api_key: String, api_secret: String) -> Self {
65 Self {
66 api_key: api_key.into_boxed_str(),
67 api_secret: api_secret.into_bytes().into_boxed_slice(),
68 }
69 }
70
71 #[must_use]
73 pub fn resolve(
74 api_key: Option<String>,
75 api_secret: Option<String>,
76 environment: BitmexEnvironment,
77 ) -> Option<Self> {
78 let (key_var, secret_var) = credential_env_vars(environment);
79 let (k, s) = resolve_env_var_pair(api_key, api_secret, key_var, secret_var)?;
80 Some(Self::new(k, s))
81 }
82
83 #[must_use]
85 pub fn api_key(&self) -> &str {
86 &self.api_key
87 }
88
89 #[must_use]
91 pub fn sign(&self, verb: &str, endpoint: &str, expires: i64, data: &str) -> String {
92 let sign_message = format!("{verb}{endpoint}{expires}{data}");
93 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
94 let signature = hmac::sign(&key, sign_message.as_bytes());
95 hex::encode(signature.as_ref())
96 }
97
98 #[must_use]
103 pub fn api_key_masked(&self) -> String {
104 mask_api_key(&self.api_key)
105 }
106}
107
108#[cfg(test)]
110mod tests {
111 use rstest::rstest;
112
113 use super::*;
114 use crate::common::testing::load_test_json;
115
116 const API_KEY: &str = "LAqUlngMIQkIUjXMUreyu3qn";
117 const API_SECRET: &str = "chNOOS4KvNXR_Xq4k4c9qsfoKWvnDecLATCRlcBwyKDYnWgO";
118
119 #[rstest]
120 fn test_simple_get() {
121 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
122
123 let signature = credential.sign("GET", "/api/v1/instrument", 1518064236, "");
124
125 assert_eq!(
126 signature,
127 "c7682d435d0cfe87c16098df34ef2eb5a549d4c5a3c2b1f0f77b8af73423bf00"
128 );
129 }
130
131 #[rstest]
132 fn test_get_with_query() {
133 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
134
135 let signature = credential.sign(
136 "GET",
137 "/api/v1/instrument?filter=%7B%22symbol%22%3A+%22XBTM15%22%7D",
138 1518064237,
139 "",
140 );
141
142 assert_eq!(
143 signature,
144 "e2f422547eecb5b3cb29ade2127e21b858b235b386bfa45e1c1756eb3383919f"
145 );
146 }
147
148 #[rstest]
149 fn test_post_with_data() {
150 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
151
152 let data = load_test_json("credential_post_order.json");
153
154 let signature = credential.sign("POST", "/api/v1/order", 1518064238, data.trim_end());
155
156 assert_eq!(
157 signature,
158 "1749cd2ccae4aa49048ae09f0b95110cee706e0944e6a14ad0b3a8cb45bd336b"
159 );
160 }
161
162 #[rstest]
163 fn test_debug_redacts_secret() {
164 let credential = Credential::new(API_KEY.to_string(), API_SECRET.to_string());
165 let dbg_out = format!("{credential:?}");
166 assert!(dbg_out.contains("api_secret: \"<redacted>\""));
167 assert!(!dbg_out.contains("chNOO"));
168 let secret_bytes_dbg = format!("{:?}", API_SECRET.as_bytes());
169 assert!(
170 !dbg_out.contains(&secret_bytes_dbg),
171 "Debug output must not contain raw secret bytes"
172 );
173 }
174
175 use crate::common::enums::BitmexEnvironment;
176
177 #[rstest]
178 fn test_resolve_with_both_args() {
179 let result = Credential::resolve(
180 Some("my_key".to_string()),
181 Some("my_secret".to_string()),
182 BitmexEnvironment::Mainnet,
183 );
184
185 assert!(result.is_some());
186 assert_eq!(result.unwrap().api_key(), "my_key");
187 }
188
189 #[rstest]
190 fn test_resolve_with_no_args_no_env() {
191 let (key_var, secret_var) = credential_env_vars(BitmexEnvironment::Mainnet);
192 if std::env::var(key_var).is_ok() || std::env::var(secret_var).is_ok() {
193 return;
194 }
195
196 let result = Credential::resolve(None, None, BitmexEnvironment::Mainnet);
197
198 assert!(result.is_none());
199 }
200}