nautilus_bybit/common/
credential.rs1#![allow(unused_assignments)] use std::fmt::{Debug, Formatter};
21
22use aws_lc_rs::hmac;
23use hex;
24use ustr::Ustr;
25use zeroize::ZeroizeOnDrop;
26
27#[derive(Clone, ZeroizeOnDrop)]
29pub struct Credential {
30 #[zeroize(skip)]
31 api_key: Ustr,
32 api_secret: Box<[u8]>,
33}
34
35impl Debug for Credential {
36 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct("Credential")
38 .field("api_key", &self.api_key)
39 .field("api_secret", &"<redacted>")
40 .finish()
41 }
42}
43
44impl Credential {
45 #[must_use]
47 pub fn new(api_key: impl Into<String>, api_secret: impl Into<String>) -> Self {
48 let api_key = api_key.into();
49 let api_secret_bytes = api_secret.into().into_bytes();
50
51 let api_key = Ustr::from(&api_key);
52
53 Self {
54 api_key,
55 api_secret: api_secret_bytes.into_boxed_slice(),
56 }
57 }
58
59 #[must_use]
61 pub fn api_key(&self) -> &Ustr {
62 &self.api_key
63 }
64
65 #[must_use]
70 pub fn api_key_masked(&self) -> String {
71 nautilus_core::string::mask_api_key(self.api_key.as_str())
72 }
73
74 #[must_use]
78 pub fn sign_websocket_auth(&self, expires: i64) -> String {
79 let message = format!("GET/realtime{expires}");
80 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
81 let tag = hmac::sign(&key, message.as_bytes());
82 hex::encode(tag.as_ref())
83 }
84
85 #[must_use]
91 pub fn sign_with_payload(
92 &self,
93 timestamp: &str,
94 recv_window_ms: u64,
95 payload: Option<&str>,
96 ) -> String {
97 let recv_window = recv_window_ms.to_string();
98 let payload_len = payload.map_or(0usize, str::len);
99 let mut message = String::with_capacity(
100 timestamp.len() + self.api_key.len() + recv_window.len() + payload_len,
101 );
102
103 message.push_str(timestamp);
104 message.push_str(self.api_key.as_str());
105 message.push_str(&recv_window);
106 if let Some(payload) = payload {
107 message.push_str(payload);
108 }
109
110 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
111 let tag = hmac::sign(&key, message.as_bytes());
112 hex::encode(tag.as_ref())
113 }
114}
115
116#[cfg(test)]
121mod tests {
122 use rstest::rstest;
123
124 use super::*;
125
126 const API_KEY: &str = "test_api_key";
127 const API_SECRET: &str = "test_secret";
128 const RECV_WINDOW: u64 = 5_000;
129 const TIMESTAMP: &str = "1700000000000";
130
131 #[rstest]
132 fn sign_with_payload_matches_reference_get() {
133 let credential = Credential::new(API_KEY, API_SECRET);
134 let query = "category=linear&symbol=BTCUSDT";
135
136 let signature = credential.sign_with_payload(TIMESTAMP, RECV_WINDOW, Some(query));
137
138 assert_eq!(
139 signature,
140 "fd4f31228a46109dc6673062328693696df9a96c7ff04e6491a45e7f63a0fdd7"
141 );
142 }
143
144 #[rstest]
145 fn sign_with_payload_matches_reference_post() {
146 let credential = Credential::new(API_KEY, API_SECRET);
147 let body = "{\"category\": \"linear\", \"symbol\": \"BTCUSDT\", \"orderLinkId\": \"test-order-1\"}";
148
149 let signature = credential.sign_with_payload(TIMESTAMP, RECV_WINDOW, Some(body));
150
151 assert_eq!(
152 signature,
153 "2df4a0603d69c08d5dea29ba85b46eb7db64ce9e9ebd34a7802a3d69700cb2a1"
154 );
155 }
156
157 #[rstest]
158 fn sign_with_empty_payload_omits_tail() {
159 let credential = Credential::new(API_KEY, API_SECRET);
160
161 let signature = credential.sign_with_payload(TIMESTAMP, RECV_WINDOW, None);
162
163 let expected = credential.sign_with_payload(TIMESTAMP, RECV_WINDOW, Some(""));
164 assert_eq!(signature, expected);
165 }
166
167 #[rstest]
168 fn sign_websocket_auth_matches_reference() {
169 let credential = Credential::new(API_KEY, API_SECRET);
170 let expires: i64 = 1_700_000_000_000;
171
172 let signature = credential.sign_websocket_auth(expires);
173
174 assert_eq!(
175 signature,
176 "bacffe7500499eb829bb58c45d36d1b3e5ac67c14eaeba91df5e99ccee013925"
177 );
178 }
179}