1use hmac::{Hmac, KeyInit, Mac};
2use sha2::Sha256;
3use std::fmt;
4
5use crate::Timestamp;
6
7#[derive(Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
8pub struct SensitiveString(String);
9
10impl fmt::Display for SensitiveString {
11 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
12 write!(f, "REDACTED")
13 }
14}
15
16impl fmt::Debug for SensitiveString {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 write!(f, "{:?}", "REDACTED")
19 }
20}
21
22impl std::ops::Deref for SensitiveString {
23 type Target = str;
24 fn deref(&self) -> &Self::Target {
25 &self.0
26 }
27}
28
29impl From<String> for SensitiveString {
30 fn from(s: String) -> Self {
31 SensitiveString(s)
32 }
33}
34
35impl From<&str> for SensitiveString {
36 fn from(s: &str) -> Self {
37 SensitiveString(s.to_string())
38 }
39}
40
41impl AsRef<str> for SensitiveString {
42 fn as_ref(&self) -> &str {
43 &self.0
44 }
45}
46
47impl SensitiveString {
48 pub fn expose(&self) -> &str {
49 &self.0
50 }
51}
52
53pub fn hmac_sha256(key: impl AsRef<[u8]>, message: impl AsRef<[u8]>) -> String {
69 let mut mac = Hmac::<Sha256>::new_from_slice(key.as_ref()).unwrap();
70 mac.update(message.as_ref());
71 let mac = mac.finalize().into_bytes();
72 hex::encode(mac)
73}
74
75type Timer = fn() -> Timestamp;
76
77#[derive(Debug)]
78pub struct Signer {
79 api_key: SensitiveString,
80 api_secret: SensitiveString,
81 recv_window: Timestamp,
83 timer: Timer,
84}
85
86impl Signer {
87 pub fn new(
89 api_key: SensitiveString,
90 api_secret: SensitiveString,
91 recv_window: Timestamp,
92 timer: Option<Timer>,
93 ) -> Self {
94 Self {
95 api_key,
96 api_secret,
97 recv_window,
98 timer: timer.unwrap_or(timestamp),
99 }
100 }
101
102 pub fn sign(&self, s: &str) -> (String, String) {
104 let timestamp = (self.timer)().to_string();
105 let api_key = self.api_key.expose();
106 let api_secret = self.api_secret.expose();
107 let message = format!("{timestamp}{}{}{s}", api_key, &self.recv_window);
108
109 let signature = hmac_sha256(api_secret, message);
110
111 (signature, timestamp)
112 }
113}
114
115pub fn timestamp() -> Timestamp {
117 std::time::UNIX_EPOCH.elapsed().unwrap().as_millis() as Timestamp
118}
119
120pub fn create_stream_signature(expires: Timestamp, api_secret: SensitiveString) -> String {
121 let api_secret = api_secret.expose();
122 let message = format!("GET/realtime{expires}");
123 hmac_sha256(api_secret, message)
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn sign_get_request() {
132 let api_key = SensitiveString("API_KEY".to_string());
133 let api_secret = SensitiveString("API_SECRET".to_string());
134 let recv_window = 5000;
135 let signer = Signer::new(api_key, api_secret, recv_window, Some(|| 1658384314791));
136 let query = "category=option&symbol=BTC-29JUL22-25000-C";
137 let expected = "8b050bb7b9d53a91c42e16a7aa94a485d7aad9c7d08023a6249a184331a52ae7";
138
139 let (signature, timestamp) = signer.sign(query);
140
141 assert_eq!(signature, expected);
142 assert_eq!(timestamp.len(), 13);
143 }
144
145 #[test]
146 fn sign_post_request() {
147 let api_key = SensitiveString("API_KEY".to_string());
148 let api_secret = SensitiveString("API_SECRET".to_string());
149 let recv_window = 5000;
150 let signer = Signer::new(api_key, api_secret, recv_window, Some(|| 1658385579423));
151 let json = r#"{"category":"option"}"#;
152 let expected = "1b9b318f05208c9113f2612b2a6d76ca29427e6d8148937c03d6505f8c00804c";
153
154 let (signature, timestamp) = signer.sign(&json);
155
156 assert_eq!(signature, expected);
157 assert_eq!(timestamp.len(), 13);
158 }
159}