rusty_feeder/exchange/bithumb/
auth.rs1use anyhow::{Result, anyhow};
7use base64::{Engine as _, engine::general_purpose};
8use hmac::{Hmac, Mac};
9use sha2::Sha512;
10use smartstring::alias::String;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13type HmacSha512 = Hmac<Sha512>;
15
16#[derive(Debug, Clone)]
18pub struct BithumbAuthConfig {
19 pub api_key: String,
21
22 pub api_secret: String,
24}
25
26impl BithumbAuthConfig {
27 #[must_use]
29 pub const fn new(api_key: String, api_secret: String) -> Self {
30 Self {
31 api_key,
32 api_secret,
33 }
34 }
35}
36
37#[derive(Debug)]
39pub struct BithumbAuth {
40 config: BithumbAuthConfig,
42}
43
44impl BithumbAuth {
45 #[must_use]
47 pub const fn new(config: BithumbAuthConfig) -> Self {
48 Self { config }
49 }
50
51 #[inline]
56 pub fn generate_nonce() -> u64 {
57 SystemTime::now()
58 .duration_since(UNIX_EPOCH)
59 .unwrap_or_default()
60 .as_millis() as u64
61 }
62
63 pub fn encode_params_zero_copy(input: &str, buffer: &mut String) -> Result<()> {
67 buffer.clear();
68 const HEX: &[u8; 16] = b"0123456789ABCDEF";
69 for &byte in input.as_bytes() {
70 match byte {
71 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
72 buffer.push(byte as char)
73 }
74 _ => {
75 buffer.push('%');
76 buffer.push(HEX[(byte >> 4) as usize] as char);
77 buffer.push(HEX[(byte & 0x0F) as usize] as char);
78 }
79 }
80 }
81 Ok(())
82 }
83
84 pub fn generate_signature(&self, endpoint: &str, params: &str, nonce: u64) -> Result<String> {
94 let prehash = format!("{endpoint}\0{params}\0{nonce}");
97
98 let mut mac = HmacSha512::new_from_slice(self.config.api_secret.as_bytes())
100 .map_err(|e| anyhow!("Failed to create HMAC: {}", e))?;
101
102 mac.update(prehash.as_bytes());
104
105 let result = mac.finalize();
107 let signature = general_purpose::STANDARD.encode(result.into_bytes());
108
109 Ok(signature.into())
110 }
111
112 pub fn build_param_string(params: &[(&str, &str)]) -> Result<String> {
120 if params.is_empty() {
121 return Ok(String::new());
122 }
123
124 let mut encoded = String::new();
125 let mut param_parts = Vec::with_capacity(params.len());
126 for (k, v) in params {
127 Self::encode_params_zero_copy(v, &mut encoded)?;
128 param_parts.push(format!("{k}={encoded}"));
129 }
130
131 Ok(param_parts.join("&").into())
132 }
133
134 pub fn generate_auth_headers(
143 &self,
144 endpoint: &str,
145 params: Option<&[(&str, &str)]>,
146 ) -> Result<(String, String, u64)> {
147 let nonce = Self::generate_nonce();
149
150 let param_string = if let Some(p) = params {
152 Self::build_param_string(p)?
153 } else {
154 String::new()
155 };
156
157 let signature = self.generate_signature(endpoint, ¶m_string, nonce)?;
159
160 Ok((self.config.api_key.clone(), signature, nonce))
161 }
162
163 pub fn generate_websocket_auth(&self) -> Result<BithumbWebSocketAuth> {
168 let nonce = Self::generate_nonce();
169
170 let endpoint = "";
172 let params = "";
173
174 let signature = self.generate_signature(endpoint, params, nonce)?;
175
176 Ok(BithumbWebSocketAuth {
177 api_key: self.config.api_key.clone(),
178 api_sign: signature,
179 api_nonce: nonce,
180 })
181 }
182}
183
184#[derive(Debug, Clone)]
186pub struct BithumbWebSocketAuth {
187 pub api_key: String,
189
190 pub api_sign: String,
192
193 pub api_nonce: u64,
195}
196
197pub fn add_auth_to_request(
199 headers: &mut reqwest::header::HeaderMap,
200 api_key: &str,
201 api_sign: &str,
202 api_nonce: u64,
203) -> Result<()> {
204 use reqwest::header::{HeaderName, HeaderValue};
205
206 headers.insert(
207 HeaderName::from_static("api-key"),
208 HeaderValue::from_str(api_key).map_err(|e| anyhow!("Invalid API key: {}", e))?,
209 );
210
211 headers.insert(
212 HeaderName::from_static("api-sign"),
213 HeaderValue::from_str(api_sign).map_err(|e| anyhow!("Invalid API signature: {}", e))?,
214 );
215
216 headers.insert(
217 HeaderName::from_static("api-nonce"),
218 HeaderValue::from_str(&api_nonce.to_string())
219 .map_err(|e| anyhow!("Invalid API nonce: {}", e))?,
220 );
221
222 Ok(())
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_nonce_generation() {
231 let nonce1 = BithumbAuth::generate_nonce();
232 std::thread::sleep(std::time::Duration::from_millis(1));
233 let nonce2 = BithumbAuth::generate_nonce();
234
235 assert!(nonce2 > nonce1);
237 }
238
239 #[test]
240 fn test_signature_generation() {
241 let config = BithumbAuthConfig::new("test_api_key".into(), "test_api_secret".into());
242
243 let auth = BithumbAuth::new(config);
244
245 let endpoint = "/info/balance";
247 let params = "currency=BTC";
248 let nonce = 1234567890123u64;
249
250 let signature = auth.generate_signature(endpoint, params, nonce).unwrap();
251 assert!(!signature.is_empty());
252
253 assert!(general_purpose::STANDARD.decode(&signature).is_ok());
255 }
256
257 #[test]
258 fn test_param_string_building() {
259 let params = vec![
260 ("currency", "BTC"),
261 ("order_currency", "KRW"),
262 ("payment_currency", "KRW"),
263 ];
264
265 let param_string = BithumbAuth::build_param_string(¶ms).unwrap();
266 assert_eq!(
267 param_string,
268 "currency=BTC&order_currency=KRW&payment_currency=KRW"
269 );
270 }
271
272 #[test]
273 fn test_auth_headers_generation() {
274 let config = BithumbAuthConfig::new("test_api_key".into(), "test_api_secret".into());
275
276 let auth = BithumbAuth::new(config);
277
278 let endpoint = "/info/balance";
279 let params = vec![("currency", "BTC")];
280
281 let (api_key, api_sign, api_nonce) =
282 auth.generate_auth_headers(endpoint, Some(¶ms)).unwrap();
283
284 assert_eq!(api_key, "test_api_key");
285 assert!(!api_sign.is_empty());
286 assert!(api_nonce > 0);
287 }
288}