nautilus_bybit/common/
credential.rs1use std::fmt::Debug;
19
20use aws_lc_rs::hmac;
21use hex;
22use ustr::Ustr;
23use zeroize::ZeroizeOnDrop;
24
25#[derive(Clone, ZeroizeOnDrop)]
27pub struct Credential {
28 #[zeroize(skip)]
29 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: impl Into<String>, api_secret: impl Into<String>) -> Self {
46 let api_key = api_key.into();
47 let api_secret_bytes = api_secret.into().into_bytes();
48
49 let api_key = Ustr::from(api_key.as_str());
50
51 Self {
52 api_key,
53 api_secret: api_secret_bytes.into_boxed_slice(),
54 }
55 }
56
57 #[must_use]
59 pub fn api_key(&self) -> &Ustr {
60 &self.api_key
61 }
62
63 #[must_use]
67 pub fn sign_websocket_auth(&self, expires: i64) -> String {
68 let message = format!("GET/realtime{expires}");
69 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
70 let tag = hmac::sign(&key, message.as_bytes());
71 hex::encode(tag.as_ref())
72 }
73
74 #[must_use]
80 pub fn sign_with_payload(
81 &self,
82 timestamp: &str,
83 recv_window_ms: u64,
84 payload: Option<&str>,
85 ) -> String {
86 let recv_window = recv_window_ms.to_string();
87 let payload_len = payload.map_or(0usize, str::len);
88 let mut message = String::with_capacity(
89 timestamp.len() + self.api_key.len() + recv_window.len() + payload_len,
90 );
91
92 message.push_str(timestamp);
93 message.push_str(self.api_key.as_str());
94 message.push_str(&recv_window);
95 if let Some(payload) = payload {
96 message.push_str(payload);
97 }
98
99 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
100 let tag = hmac::sign(&key, message.as_bytes());
101 hex::encode(tag.as_ref())
102 }
103}
104
105#[cfg(test)]
110mod tests {
111 use rstest::rstest;
112
113 use super::*;
114
115 const API_KEY: &str = "test_api_key";
116 const API_SECRET: &str = "test_secret";
117 const RECV_WINDOW: u64 = 5_000;
118 const TIMESTAMP: &str = "1700000000000";
119
120 #[rstest]
121 fn sign_with_payload_matches_reference_get() {
122 let credential = Credential::new(API_KEY, API_SECRET);
123 let query = "category=linear&symbol=BTCUSDT";
124
125 let signature = credential.sign_with_payload(TIMESTAMP, RECV_WINDOW, Some(query));
126
127 assert_eq!(
128 signature,
129 "fd4f31228a46109dc6673062328693696df9a96c7ff04e6491a45e7f63a0fdd7"
130 );
131 }
132
133 #[rstest]
134 fn sign_with_payload_matches_reference_post() {
135 let credential = Credential::new(API_KEY, API_SECRET);
136 let body = "{\"category\": \"linear\", \"symbol\": \"BTCUSDT\", \"orderLinkId\": \"test-order-1\"}";
137
138 let signature = credential.sign_with_payload(TIMESTAMP, RECV_WINDOW, Some(body));
139
140 assert_eq!(
141 signature,
142 "2df4a0603d69c08d5dea29ba85b46eb7db64ce9e9ebd34a7802a3d69700cb2a1"
143 );
144 }
145
146 #[rstest]
147 fn sign_with_empty_payload_omits_tail() {
148 let credential = Credential::new(API_KEY, API_SECRET);
149
150 let signature = credential.sign_with_payload(TIMESTAMP, RECV_WINDOW, None);
151
152 let expected = credential.sign_with_payload(TIMESTAMP, RECV_WINDOW, Some(""));
153 assert_eq!(signature, expected);
154 }
155
156 #[rstest]
157 fn sign_websocket_auth_matches_reference() {
158 let credential = Credential::new(API_KEY, API_SECRET);
159 let expires: i64 = 1_700_000_000_000;
160
161 let signature = credential.sign_websocket_auth(expires);
162
163 assert_eq!(
164 signature,
165 "bacffe7500499eb829bb58c45d36d1b3e5ac67c14eaeba91df5e99ccee013925"
166 );
167 }
168}