nautilus_okx/common/
credential.rs1use std::fmt::Debug;
19
20use aws_lc_rs::hmac;
21use base64::prelude::*;
22use ustr::Ustr;
23use zeroize::ZeroizeOnDrop;
24
25#[derive(Clone, ZeroizeOnDrop)]
30pub struct Credential {
31 #[zeroize(skip)]
32 pub api_key: Ustr,
33 pub api_passphrase: String,
34 api_secret: Box<[u8]>,
35}
36
37impl Debug for Credential {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct(stringify!(Credential))
40 .field("api_key", &self.api_key)
41 .field("api_passphrase", &self.api_passphrase)
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, api_passphrase: String) -> Self {
51 Self {
52 api_key: api_key.into(),
53 api_passphrase,
54 api_secret: api_secret.into_bytes().into_boxed_slice(),
55 }
56 }
57
58 pub fn sign(&self, timestamp: &str, method: &str, endpoint: &str, body: &str) -> String {
64 self.sign_bytes(timestamp, method, endpoint, Some(body.as_bytes()))
65 }
66
67 pub fn sign_bytes(
70 &self,
71 timestamp: &str,
72 method: &str,
73 endpoint: &str,
74 body: Option<&[u8]>,
75 ) -> String {
76 let mut message = Vec::with_capacity(
77 timestamp.len() + method.len() + endpoint.len() + body.map(|b| b.len()).unwrap_or(0),
78 );
79 message.extend_from_slice(timestamp.as_bytes());
80 message.extend_from_slice(method.as_bytes());
81 message.extend_from_slice(endpoint.as_bytes());
82 if let Some(b) = body {
83 message.extend_from_slice(b);
84 }
85
86 let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret[..]);
87 let tag = hmac::sign(&key, &message);
88 BASE64_STANDARD.encode(tag.as_ref())
89 }
90}
91
92#[cfg(test)]
97mod tests {
98 use rstest::rstest;
99
100 use super::*;
101
102 const API_KEY: &str = "985d5b66-57ce-40fb-b714-afc0b9787083";
103 const API_SECRET: &str = "chNOOS4KvNXR_Xq4k4c9qsfoKWvnDecLATCRlcBwyKDYnWgO";
104 const API_PASSPHRASE: &str = "1234567890";
105
106 #[rstest]
107 fn test_simple_get() {
108 let credential = Credential::new(
109 API_KEY.to_string(),
110 API_SECRET.to_string(),
111 API_PASSPHRASE.to_string(),
112 );
113
114 let signature = credential.sign(
115 "2020-12-08T09:08:57.715Z",
116 "GET",
117 "/api/v5/account/balance",
118 "",
119 );
120
121 assert_eq!(signature, "PJ61e1nb2F2Qd7D8SPiaIcx2gjdELc+o0ygzre9z33k=");
122 }
123
124 #[rstest]
125 fn test_get_with_query_params() {
126 let credential = Credential::new(
127 API_KEY.to_string(),
128 API_SECRET.to_string(),
129 API_PASSPHRASE.to_string(),
130 );
131
132 let signature = credential.sign(
133 "2020-12-08T09:08:57.715Z",
134 "GET",
135 "/api/v5/account/balance?ccy=BTC",
136 "",
137 );
138
139 assert!(!signature.is_empty());
140 assert!(BASE64_STANDARD.decode(&signature).is_ok());
141
142 let expected_message = "2020-12-08T09:08:57.715ZGET/api/v5/account/balance?ccy=BTC";
144
145 let key = hmac::Key::new(hmac::HMAC_SHA256, API_SECRET.as_bytes());
147 let tag = hmac::sign(&key, expected_message.as_bytes());
148 let expected_signature = BASE64_STANDARD.encode(tag.as_ref());
149 assert_eq!(signature, expected_signature);
150 }
151
152 #[rstest]
153 fn test_post_with_json_body() {
154 let credential = Credential::new(
155 API_KEY.to_string(),
156 API_SECRET.to_string(),
157 API_PASSPHRASE.to_string(),
158 );
159
160 let body = r#"{"instId":"BTC-USD-200925","tdMode":"isolated","side":"buy","ordType":"limit","px":"432.11","sz":"2"}"#;
162 let signature = credential.sign(
163 "2020-12-08T09:08:57.715Z",
164 "POST",
165 "/api/v5/trade/order",
166 body,
167 );
168
169 assert!(!signature.is_empty());
170 assert!(BASE64_STANDARD.decode(&signature).is_ok());
171 }
172
173 #[rstest]
174 fn test_post_algo_order() {
175 let credential = Credential::new(
176 API_KEY.to_string(),
177 API_SECRET.to_string(),
178 API_PASSPHRASE.to_string(),
179 );
180
181 let body = r#"[{"instId":"ETH-USDT-SWAP","tdMode":"isolated","side":"buy","ordType":"trigger","sz":"0.01","triggerPx":"3000","orderPx":"-1","triggerPxType":"last"}]"#;
183 let signature = credential.sign(
184 "2025-01-20T10:30:45.123Z",
185 "POST",
186 "/api/v5/trade/order-algo",
187 body,
188 );
189
190 assert!(!signature.is_empty());
191 assert!(BASE64_STANDARD.decode(&signature).is_ok());
192
193 let expected_message = format!(
195 "2025-01-20T10:30:45.123ZPOST/api/v5/trade/order-algo{}",
196 body
197 );
198
199 let key = hmac::Key::new(hmac::HMAC_SHA256, API_SECRET.as_bytes());
201 let tag = hmac::sign(&key, expected_message.as_bytes());
202 let expected_signature = BASE64_STANDARD.encode(tag.as_ref());
203 assert_eq!(signature, expected_signature);
204 }
205
206 #[rstest]
207 fn test_debug_redacts_secret() {
208 let credential = Credential::new(
209 API_KEY.to_string(),
210 API_SECRET.to_string(),
211 API_PASSPHRASE.to_string(),
212 );
213 let dbg_out = format!("{:?}", credential);
214 assert!(dbg_out.contains("api_secret: \"<redacted>\""));
215 assert!(!dbg_out.contains("chNOO"));
216 let secret_bytes_dbg = format!("{:?}", API_SECRET.as_bytes());
217 assert!(
218 !dbg_out.contains(&secret_bytes_dbg),
219 "Debug output must not contain raw secret bytes"
220 );
221 }
222}