1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use thiserror::Error;
use time::OffsetDateTime;

/// Sign error.
#[derive(Debug, Error)]
pub enum SignError {
    /// Invalid length.
    #[error("invalid length of secretkey")]
    InvalidLength,
    /// Urlencoded.
    #[error("urlencoded: {0}")]
    Urlencoded(#[from] serde_urlencoded::ser::Error),
}

type HmacSha256 = Hmac<Sha256>;

/// Binance API Key.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BinanceKey {
    /// Apikey.
    pub apikey: String,
    /// Secretkey.
    pub secretkey: String,
}

impl BinanceKey {
    /// Sign.
    pub fn sign<T: Serialize>(&self, params: T) -> Result<SignedParams<T>, SignError> {
        SigningParams::now(params).signed(&self)
    }
}

/// Signing params.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SigningParams<T> {
    #[serde(flatten)]
    params: T,
    #[serde(rename = "recvWindow")]
    recv_window: i64,
    timestamp: i64,
}

impl<T> SigningParams<T> {
    fn with_timestamp(params: T, timestamp: i64) -> Self {
        Self {
            params,
            recv_window: 5000,
            timestamp,
        }
    }

    /// Sign the given params now.
    pub fn now(params: T) -> Self {
        let now = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000;
        Self::with_timestamp(params, now as i64)
    }
}

/// Signed params.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SignedParams<T> {
    #[serde(flatten)]
    signing: SigningParams<T>,
    signature: String,
}

impl<T: Serialize> SigningParams<T> {
    /// Get signed params.
    pub fn signed(self, key: &BinanceKey) -> Result<SignedParams<T>, SignError> {
        let raw = serde_urlencoded::to_string(&self)?;
        tracing::debug!("raw string to sign: {}", raw);
        let mut mac = HmacSha256::new_from_slice(key.secretkey.as_bytes())
            .map_err(|_| SignError::InvalidLength)?;
        mac.update(raw.as_bytes());
        let signature = hex::encode(mac.finalize().into_bytes());
        Ok(SignedParams {
            signing: self,
            signature,
        })
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[derive(Debug, Serialize)]
    struct Params {
        symbol: String,
        side: String,
        #[serde(rename = "type")]
        kind: String,
        #[serde(rename = "timeInForce")]
        time_in_force: String,
        quantity: i64,
        price: f64,
    }

    #[test]
    fn test_hmac_sha256() -> anyhow::Result<()> {
        let key = "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j";
        let raw = "asset=ETH&address=0x6915f16f8791d0a1cc2bf47c13a6b2a92000504b&amount=1&recvWindow=5000&name=test&timestamp=1510903211000";
        let mut mac = HmacSha256::new_from_slice(key.as_bytes())?;
        mac.update(raw.as_bytes());
        let encoded = hex::encode(mac.finalize().into_bytes());
        println!("{}", encoded);
        assert_eq!(
            encoded,
            "157fb937ec848b5f802daa4d9f62bea08becbf4f311203bda2bd34cd9853e320"
        );
        Ok(())
    }

    #[test]
    fn test_signature() -> anyhow::Result<()> {
        let key = BinanceKey {
            apikey: "".to_string(),
            secretkey: "NhqPtmdSJYdKjVHjA7PZj4Mge3R5YNiP1e3UZjInClVN65XAbvqqM6A7H5fATj0j"
                .to_string(),
        };
        let params = Params {
            symbol: "LTCBTC".to_string(),
            side: "BUY".to_string(),
            kind: "LIMIT".to_string(),
            time_in_force: "GTC".to_string(),
            quantity: 1,
            price: 0.1,
        };
        println!("raw={}", serde_urlencoded::to_string(&params)?);
        let signed = SigningParams::with_timestamp(params, 1499827319559).signed(&key)?;
        let target = serde_json::json!({
            "symbol": "LTCBTC",
            "side": "BUY",
            "type": "LIMIT",
            "timeInForce": "GTC",
            "quantity": 1,
            "price": 0.1,
            "recvWindow": 5000,
            "timestamp": 1499827319559_i64,
            "signature": "c8db56825ae71d6d79447849e617115f4a920fa2acdcab2b053c4b2838bd6b71",
        });
        assert_eq!(serde_json::to_value(&signed)?, target);
        Ok(())
    }
}