exc_binance/types/
key.rs

1use hmac::{Hmac, Mac};
2use serde::{Deserialize, Serialize};
3use sha2::Sha256;
4use thiserror::Error;
5use time::OffsetDateTime;
6
7/// Sign error.
8#[derive(Debug, Error)]
9pub enum SignError {
10    /// Invalid length.
11    #[error("invalid length of secretkey")]
12    InvalidLength,
13    /// Urlencoded.
14    #[error("urlencoded: {0}")]
15    Urlencoded(#[from] serde_urlencoded::ser::Error),
16}
17
18type HmacSha256 = Hmac<Sha256>;
19
20/// Binance API Key.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct BinanceKey {
23    /// Apikey.
24    pub apikey: String,
25    /// Secretkey.
26    pub secretkey: String,
27}
28
29impl BinanceKey {
30    /// Sign.
31    pub fn sign<T: Serialize>(&self, params: T) -> Result<SignedParams<T>, SignError> {
32        SigningParams::now(params).signed(self)
33    }
34}
35
36/// Signing params.
37#[derive(Debug, Clone, Deserialize, Serialize)]
38pub struct SigningParams<T> {
39    #[serde(flatten)]
40    params: T,
41    #[serde(rename = "recvWindow")]
42    recv_window: i64,
43    timestamp: i64,
44}
45
46impl<T> SigningParams<T> {
47    fn with_timestamp(params: T, timestamp: i64) -> Self {
48        Self {
49            params,
50            recv_window: 5000,
51            timestamp,
52        }
53    }
54
55    /// Sign the given params now.
56    pub fn now(params: T) -> Self {
57        let now = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000;
58        Self::with_timestamp(params, now as i64)
59    }
60}
61
62/// Signed params.
63#[derive(Debug, Clone, Deserialize, Serialize)]
64pub struct SignedParams<T> {
65    #[serde(flatten)]
66    signing: SigningParams<T>,
67    signature: String,
68}
69
70impl<T: Serialize> SigningParams<T> {
71    /// Get signed params.
72    pub fn signed(self, key: &BinanceKey) -> Result<SignedParams<T>, SignError> {
73        let raw = serde_urlencoded::to_string(&self)?;
74        tracing::debug!("raw string to sign: {}", raw);
75        let mut mac = HmacSha256::new_from_slice(key.secretkey.as_bytes())
76            .map_err(|_| SignError::InvalidLength)?;
77        mac.update(raw.as_bytes());
78        let signature = hex::encode(mac.finalize().into_bytes());
79        Ok(SignedParams {
80            signing: self,
81            signature,
82        })
83    }
84}
85
86#[cfg(test)]
87mod test {
88    use super::*;
89
90    #[derive(Debug, Serialize)]
91    struct Params {
92        symbol: String,
93        side: String,
94        #[serde(rename = "type")]
95        kind: String,
96        #[serde(rename = "timeInForce")]
97        time_in_force: String,
98        quantity: i64,
99        price: f64,
100    }
101
102    #[test]
103    fn test_hmac_sha256() -> anyhow::Result<()> {
104        let key = "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j";
105        let raw = "asset=ETH&address=0x6915f16f8791d0a1cc2bf47c13a6b2a92000504b&amount=1&recvWindow=5000&name=test&timestamp=1510903211000";
106        let mut mac = HmacSha256::new_from_slice(key.as_bytes())?;
107        mac.update(raw.as_bytes());
108        let encoded = hex::encode(mac.finalize().into_bytes());
109        println!("{encoded}");
110        assert_eq!(
111            encoded,
112            "157fb937ec848b5f802daa4d9f62bea08becbf4f311203bda2bd34cd9853e320"
113        );
114        Ok(())
115    }
116
117    #[test]
118    fn test_signature() -> anyhow::Result<()> {
119        let key = BinanceKey {
120            apikey: "".to_string(),
121            secretkey: "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j"
122                .to_string(),
123        };
124        let params = Params {
125            symbol: "LTCBTC".to_string(),
126            side: "BUY".to_string(),
127            kind: "LIMIT".to_string(),
128            time_in_force: "GTC".to_string(),
129            quantity: 1,
130            price: 0.1,
131        };
132        println!("raw={}", serde_urlencoded::to_string(&params)?);
133        let signed = SigningParams::with_timestamp(params, 1499827319559).signed(&key)?;
134        let target = serde_json::json!({
135            "symbol": "LTCBTC",
136            "side": "BUY",
137            "type": "LIMIT",
138            "timeInForce": "GTC",
139            "quantity": 1,
140            "price": 0.1,
141            "recvWindow": 5000,
142            "timestamp": 1499827319559_i64,
143            "signature": "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71",
144        });
145        assert_eq!(serde_json::to_value(signed)?, target);
146        Ok(())
147    }
148}