Skip to main content

indodax_cli/
auth.rs

1use hmac::{Hmac, Mac};
2use sha2::Sha512;
3use std::sync::atomic::{AtomicU64, Ordering};
4
5use crate::errors::IndodaxError;
6
7use std::fmt;
8
9pub struct Signer {
10    api_key: String,
11    secret_key: String,
12    last_nonce: AtomicU64,
13}
14
15impl fmt::Debug for Signer {
16    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17        f.debug_struct("Signer")
18            .field("api_key", &"****")
19            .field("secret_key", &"****")
20            .field("last_nonce", &self.last_nonce)
21            .finish()
22    }
23}
24
25impl Signer {
26    pub fn new(api_key: &str, secret_key: &str) -> Self {
27        Self {
28            api_key: api_key.to_string(),
29            secret_key: secret_key.to_string(),
30            last_nonce: AtomicU64::new(0),
31        }
32    }
33
34    pub fn api_key(&self) -> &str {
35        &self.api_key
36    }
37
38    pub fn next_nonce_str(&self) -> String {
39        self.next_nonce().to_string()
40    }
41
42    fn next_nonce(&self) -> u64 {
43        let now = crate::now_millis();
44        loop {
45            let prev = self.last_nonce.load(Ordering::Acquire);
46            let next = if now > prev { now } else { prev + 1 };
47            if self
48                .last_nonce
49                .compare_exchange(prev, next, Ordering::Release, Ordering::Relaxed)
50                .is_ok()
51            {
52                return next;
53            }
54        }
55    }
56
57    pub fn sign_v1(&self, payload: &str) -> Result<(String, String), IndodaxError> {
58        let signature = self.hmac_sha512(payload, &self.secret_key)?;
59        let encoded_sign = hex::encode(signature);
60
61        Ok((payload.to_string(), encoded_sign))
62    }
63
64    pub fn sign_v2(&self, query_string: &str) -> Result<String, IndodaxError> {
65        let signature = self.hmac_sha512(query_string, &self.secret_key)?;
66        Ok(hex::encode(signature))
67    }
68
69    fn hmac_sha512(&self, data: &str, key: &str) -> Result<Vec<u8>, IndodaxError> {
70        let mut mac = Hmac::<Sha512>::new_from_slice(key.as_bytes())
71            .map_err(|e| IndodaxError::Other(format!("HMAC initialization failed: {}", e)))?;
72        mac.update(data.as_bytes());
73        Ok(mac.finalize().into_bytes().to_vec())
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_signer_new() {
83        let signer = Signer::new("api_key", "secret_key");
84        assert_eq!(signer.api_key(), "api_key");
85    }
86
87    #[test]
88    fn test_signer_api_key() {
89        let signer = Signer::new("my_api_key", "my_secret");
90        assert_eq!(signer.api_key(), "my_api_key");
91    }
92
93    #[test]
94    fn test_signer_next_nonce_str() {
95        let signer = Signer::new("key", "secret");
96        let nonce = signer.next_nonce_str();
97        assert!(!nonce.is_empty());
98        // Nonce should be a number
99        assert!(nonce.parse::<u64>().is_ok());
100    }
101
102    #[test]
103    fn test_signer_next_nonce_is_increasing() {
104        let signer = Signer::new("key", "secret");
105        let nonce1 = signer.next_nonce();
106        let nonce2 = signer.next_nonce();
107        // Nonces should be increasing (or at least not decreasing)
108        assert!(nonce2 >= nonce1);
109    }
110
111    #[test]
112    fn test_signer_now_millis() {
113        let millis = crate::now_millis();
114        assert!(millis > 0);
115        // Should be around current time in millis
116        assert!(millis > 1_000_000_000_000); // After year 2001
117    }
118
119    #[test]
120    fn test_signer_sign_v1() {
121        let signer = Signer::new("key", "secret");
122        let (payload, signature) = signer.sign_v1("method=test&nonce=123").unwrap();
123        assert_eq!(payload, "method=test&nonce=123");
124        assert!(!signature.is_empty());
125        // Signature should be hex encoded
126        assert!(hex::decode(&signature).is_ok());
127    }
128
129    #[test]
130    fn test_signer_sign_v1_different_secrets() {
131        let signer1 = Signer::new("key", "secret1");
132        let signer2 = Signer::new("key", "secret2");
133        let (_payload, sig1) = signer1.sign_v1("test").unwrap();
134        let (_payload2, sig2) = signer2.sign_v1("test").unwrap();
135        assert_ne!(sig1, sig2);
136    }
137
138    #[test]
139    fn test_signer_sign_v2() {
140        let signer = Signer::new("key", "secret");
141        let signature = signer.sign_v2("param1=value1").unwrap();
142        assert!(!signature.is_empty());
143        // Signature should be hex encoded
144        assert!(hex::decode(&signature).is_ok());
145    }
146
147    #[test]
148    fn test_signer_sign_v2_with_timestamp() {
149        let signer = Signer::new("key", "secret");
150        let query_string = "symbol=BTCIDR";
151        let signature = signer.sign_v2(query_string).unwrap();
152
153        // We can't easily verify the HMAC without knowing the secret, but we can verify it's valid hex
154        assert!(!signature.is_empty());
155    }
156
157    #[test]
158    fn test_hmac_sha512_output_length() {
159        let signer = Signer::new("key", "secret");
160        let result = signer.hmac_sha512("test data", "secret").unwrap();
161        // SHA512 produces 64 bytes
162        assert_eq!(result.len(), 64);
163    }
164
165    #[test]
166    fn test_signer_multiple_signatures_different() {
167        let signer = Signer::new("key", "secret");
168        let (_, sig1) = signer.sign_v1("payload1").unwrap();
169        let (_, sig2) = signer.sign_v1("payload2").unwrap();
170        assert_ne!(sig1, sig2);
171    }
172}