1use hmac::{Hmac, KeyInit, Mac};
2use sha2::Sha256;
3use std::fmt::{self, Display, Formatter};
4
5use crate::Timestamp;
6
7#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
8pub struct SensitiveString(String);
9
10impl Display for SensitiveString {
11 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
12 write!(f, "REDACTED")
13 }
14}
15
16impl std::ops::Deref for SensitiveString {
17 type Target = str;
18 fn deref(&self) -> &Self::Target {
19 &self.0
20 }
21}
22
23impl From<String> for SensitiveString {
24 fn from(s: String) -> Self {
25 SensitiveString(s)
26 }
27}
28
29impl From<&str> for SensitiveString {
30 fn from(s: &str) -> Self {
31 SensitiveString(s.to_string())
32 }
33}
34
35impl AsRef<str> for SensitiveString {
36 fn as_ref(&self) -> &str {
37 &self.0
38 }
39}
40
41impl SensitiveString {
42 pub fn expose(&self) -> &str {
43 &self.0
44 }
45}
46
47pub fn hmac_sha256(key: impl AsRef<[u8]>, message: impl AsRef<[u8]>) -> String {
48 let mut mac = Hmac::<Sha256>::new_from_slice(key.as_ref()).unwrap();
49 mac.update(message.as_ref());
50 let mac = mac.finalize().into_bytes().to_vec();
51 hex::encode(&mac)
52}
53
54pub fn sign_query(api_secret: &SensitiveString, timestamp: Timestamp, query: &str) -> String {
55 let query = if query.is_empty() {
56 format!("timestamp={timestamp}")
57 } else {
58 format!("{query}×tamp={timestamp}")
59 };
60 let signature = hmac_sha256(api_secret.expose(), &query);
61 format!("{query}&signature={signature}")
62}
63
64pub fn timestamp() -> Timestamp {
66 std::time::UNIX_EPOCH.elapsed().unwrap().as_millis() as Timestamp
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn test_sign_query() {
75 let api_secret = SensitiveString(
76 "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j".to_string(),
77 );
78 let query = "symbol=LTCBTC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=1&price=0.1&recvWindow=5000×tamp=1499827319559";
79 let signature = "6497562735f592c5b199a6050647b2972e322c6daea096b47cf7b84694619206";
80 let timestamp = 123456789;
81 let expected = format!("{query}×tamp={timestamp}&signature={signature}");
82
83 let signed_query = sign_query(&api_secret, timestamp, query);
84
85 assert_eq!(expected, signed_query);
86 }
87
88 #[test]
89 fn test_sign_empty_query_has_no_leading_amp() {
90 let api_secret = SensitiveString("secret".to_string());
91 let signed = sign_query(&api_secret, 123, "");
92 assert!(
93 signed.starts_with("timestamp=123&signature="),
94 "got {signed}"
95 );
96 }
97}