1use crate::collections::FxHashMap;
8use crate::{CommonError, Result, SmartString};
9use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
10use hex;
11use std::fmt::Debug;
12
13#[derive(Debug, Clone)]
15pub struct Ed25519Auth {
16 signing_key: SigningKey,
17 verifying_key: VerifyingKey,
18 api_key: SmartString,
19}
20
21impl Ed25519Auth {
22 #[must_use]
24 pub fn new(signing_key: SigningKey, api_key: SmartString) -> Self {
25 let verifying_key = signing_key.verifying_key();
26 Self {
27 signing_key,
28 verifying_key,
29 api_key,
30 }
31 }
32
33 pub fn from_secret_key_hex(secret_hex: &str, api_key: SmartString) -> Result<Self> {
42 if secret_hex.len() != 64 {
44 return Err(CommonError::Auth(
45 format!(
46 "Invalid secret key length: expected 64 hex characters, got {}",
47 secret_hex.len()
48 )
49 .into(),
50 ));
51 }
52
53 let secret_bytes = hex::decode(secret_hex)
55 .map_err(|e| CommonError::Auth(format!("Invalid hex in secret key: {e}").into()))?;
56
57 let secret_array: [u8; 32] = secret_bytes
59 .try_into()
60 .map_err(|_| CommonError::Auth("Secret key must be exactly 32 bytes".into()))?;
61
62 let signing_key = SigningKey::from_bytes(&secret_array);
64 let verifying_key = signing_key.verifying_key();
65
66 Ok(Self {
67 signing_key,
68 verifying_key,
69 api_key,
70 })
71 }
72
73 pub fn sign_params(&self, params: &[(&str, &str)]) -> Result<SmartString> {
84 let query_string = build_query_string(params)?;
86
87 let signature = self.signing_key.sign(query_string.as_bytes());
91
92 Ok(hex::encode(signature.to_bytes()).into())
94 }
95
96 pub fn sign_request(
109 &self,
110 _method: &str,
111 _path: &str,
112 _body: Option<&str>,
113 ) -> Result<FxHashMap<SmartString, SmartString>> {
114 let mut headers = FxHashMap::default();
115
116 headers.insert("X-MBX-APIKEY".into(), self.api_key.clone());
118
119 Ok(headers)
124 }
125
126 #[must_use]
128 pub fn api_key(&self) -> &str {
129 &self.api_key
130 }
131
132 #[must_use]
134 pub fn public_key_hex(&self) -> SmartString {
135 hex::encode(self.verifying_key.to_bytes()).into()
136 }
137
138 #[must_use]
140 pub const fn signing_key(&self) -> &SigningKey {
141 &self.signing_key
142 }
143
144 #[must_use]
146 pub const fn verifying_key(&self) -> &VerifyingKey {
147 &self.verifying_key
148 }
149}
150
151pub fn generate_ed25519_signature(
163 secret_key_hex: &str,
164 params: &[(&str, &str)],
165) -> Result<SmartString> {
166 let auth = Ed25519Auth::from_secret_key_hex(secret_key_hex, "temp".into())?;
167 auth.sign_params(params)
168}
169
170fn build_query_string(params: &[(&str, &str)]) -> Result<SmartString> {
181 if params.is_empty() {
182 return Ok(SmartString::new());
183 }
184
185 let mut sorted_params = params.to_vec();
187 sorted_params.sort_by_key(|&(k, _)| k);
188
189 let mut query_parts = Vec::with_capacity(sorted_params.len());
190
191 for (key, value) in sorted_params {
192 let encoded_key = urlencoding::encode(key);
194 let encoded_value = urlencoding::encode(value);
195 query_parts.push(format!("{encoded_key}={encoded_value}"));
196 }
197
198 Ok(query_parts.join("&").into())
199}
200
201#[cfg(test)]
206pub fn generate_test_signing_key() -> SigningKey {
207 let test_key_bytes = [
209 0x4c, 0x5b, 0x2e, 0x7d, 0x3f, 0x8a, 0x9b, 0x1c, 0x6e, 0x5d, 0x4a, 0x3b, 0x2c, 0x1d, 0x0e,
210 0x9f, 0x8a, 0x7b, 0x6c, 0x5d, 0x4e, 0x3f, 0x2a, 0x1b, 0x0c, 0x9d, 0x8e, 0x7f, 0x6a, 0x5b,
211 0x4c, 0x3d,
212 ];
213 SigningKey::from_bytes(&test_key_bytes)
214}
215
216#[cfg(test)]
218pub fn create_test_auth() -> Ed25519Auth {
219 let signing_key = generate_test_signing_key();
220 Ed25519Auth::new(signing_key, "test-api-key".into())
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 #[test]
228 fn test_ed25519_auth_creation_from_hex() {
229 let secret_hex = "4c5b2e7d3f8a9b1c6e5d4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d";
231 let api_key = "test-api-key".into();
232
233 let auth = Ed25519Auth::from_secret_key_hex(secret_hex, api_key);
234 assert!(auth.is_ok());
235
236 let auth = auth.unwrap();
237 assert_eq!(auth.api_key(), "test-api-key");
238 assert_eq!(auth.public_key_hex().len(), 64); }
240
241 #[test]
242 fn test_ed25519_auth_invalid_hex_length() {
243 let short_hex = "4c5b2e7d3f8a9b1c";
245 let result = Ed25519Auth::from_secret_key_hex(short_hex, "test".into());
246 assert!(result.is_err());
247 assert!(
248 result
249 .unwrap_err()
250 .to_string()
251 .contains("Invalid secret key length")
252 );
253
254 let long_hex = "4c5b2e7d3f8a9b1c6e5d4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d1234";
256 let result = Ed25519Auth::from_secret_key_hex(long_hex, "test".into());
257 assert!(result.is_err());
258 assert!(
259 result
260 .unwrap_err()
261 .to_string()
262 .contains("Invalid secret key length")
263 );
264 }
265
266 #[test]
267 fn test_ed25519_auth_invalid_hex_chars() {
268 let invalid_hex = "4c5b2e7d3f8a9b1c6e5d4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4cZZ";
270 let result = Ed25519Auth::from_secret_key_hex(invalid_hex, "test".into());
271 assert!(result.is_err());
272 assert!(result.unwrap_err().to_string().contains("Invalid hex"));
273 }
274
275 #[test]
276 fn test_sign_params_empty() {
277 let auth = create_test_auth();
278 let signature = auth.sign_params(&[]);
279 assert!(signature.is_ok());
280
281 let sig = signature.unwrap();
282 assert_eq!(sig.len(), 128); }
284
285 #[test]
286 fn test_sign_params_single() {
287 let auth = create_test_auth();
288 let params = [("symbol", "BTCUSDT")];
289 let signature = auth.sign_params(¶ms);
290 assert!(signature.is_ok());
291
292 let sig = signature.unwrap();
293 assert_eq!(sig.len(), 128);
294 assert!(sig.chars().all(|c| c.is_ascii_hexdigit()));
295 }
296
297 #[test]
298 fn test_sign_params_multiple() {
299 let auth = create_test_auth();
300 let params = [
301 ("symbol", "BTCUSDT"),
302 ("side", "BUY"),
303 ("type", "LIMIT"),
304 ("quantity", "1"),
305 ("price", "50000"),
306 ];
307 let signature = auth.sign_params(¶ms);
308 assert!(signature.is_ok());
309
310 let sig = signature.unwrap();
311 assert_eq!(sig.len(), 128);
312 assert!(sig.chars().all(|c| c.is_ascii_hexdigit()));
313 }
314
315 #[test]
316 fn test_sign_params_with_special_chars() {
317 let auth = create_test_auth();
318 let params = [
319 ("symbol", "BTC/USDT"),
320 ("message", "Hello World!"),
321 ("special", "[email protected]"),
322 ];
323 let signature = auth.sign_params(¶ms);
324 assert!(signature.is_ok());
325
326 let sig = signature.unwrap();
327 assert_eq!(sig.len(), 128);
328 }
329
330 #[test]
331 fn test_signature_consistency() {
332 let auth = create_test_auth();
333 let params = [("symbol", "BTCUSDT"), ("price", "50000")];
334
335 let sig1 = auth.sign_params(¶ms).unwrap();
336 let sig2 = auth.sign_params(¶ms).unwrap();
337
338 assert_eq!(sig1, sig2, "Signatures should be deterministic");
339 }
340
341 #[test]
342 fn test_signature_sensitivity() {
343 let auth = create_test_auth();
344
345 let params1 = [("symbol", "BTCUSDT")];
346 let params2 = [("symbol", "ETHUSDT")]; let sig1 = auth.sign_params(¶ms1).unwrap();
349 let sig2 = auth.sign_params(¶ms2).unwrap();
350
351 assert_ne!(
352 sig1, sig2,
353 "Different parameters should produce different signatures"
354 );
355 }
356
357 #[test]
358 fn test_parameter_ordering() {
359 let auth = create_test_auth();
360
361 let params1 = [("symbol", "BTCUSDT"), ("side", "BUY")];
363 let params2 = [("side", "BUY"), ("symbol", "BTCUSDT")];
364
365 let sig1 = auth.sign_params(¶ms1).unwrap();
366 let sig2 = auth.sign_params(¶ms2).unwrap();
367
368 assert_eq!(sig1, sig2, "Parameter order should not affect signature");
369 }
370
371 #[test]
372 fn test_generate_ed25519_signature_function() {
373 let secret_hex = "4c5b2e7d3f8a9b1c6e5d4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d";
374 let params = [("symbol", "BTCUSDT"), ("side", "BUY")];
375
376 let signature = generate_ed25519_signature(secret_hex, ¶ms);
377 assert!(signature.is_ok());
378
379 let sig = signature.unwrap();
380 assert_eq!(sig.len(), 128);
381 assert!(sig.chars().all(|c| c.is_ascii_hexdigit()));
382 }
383
384 #[test]
385 fn test_sign_request_headers() {
386 let auth = create_test_auth();
387 let headers = auth.sign_request("GET", "/api/v3/account", None);
388 assert!(headers.is_ok());
389
390 let headers = headers.unwrap();
391 let api_key_header: SmartString = "X-MBX-APIKEY".into();
392 assert!(headers.contains_key(&api_key_header));
393 assert_eq!(headers.get(&api_key_header).unwrap(), "test-api-key");
394 }
395
396 #[test]
397 fn test_public_key_generation() {
398 let secret_hex = "4c5b2e7d3f8a9b1c6e5d4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d";
399 let auth = Ed25519Auth::from_secret_key_hex(secret_hex, "test".into()).unwrap();
400
401 let public_key = auth.public_key_hex();
402 assert_eq!(public_key.len(), 64); assert!(public_key.chars().all(|c| c.is_ascii_hexdigit()));
404 }
405
406 #[test]
407 fn test_build_query_string() {
408 let result = build_query_string(&[]);
410 assert!(result.is_ok());
411 assert_eq!(result.unwrap(), "");
412
413 let result = build_query_string(&[("key", "value")]);
415 assert!(result.is_ok());
416 assert_eq!(result.unwrap(), "key=value");
417
418 let result = build_query_string(&[("zebra", "last"), ("alpha", "first")]);
420 assert!(result.is_ok());
421 assert_eq!(result.unwrap(), "alpha=first&zebra=last");
422
423 let result = build_query_string(&[("symbol", "BTC/USDT"), ("message", "Hello World!")]);
425 assert!(result.is_ok());
426 let query = result.unwrap();
427 assert!(query.contains("BTC%2FUSDT")); assert!(query.contains("Hello%20World%21")); }
430}