1use hmac::{Hmac, Mac};
2use serde::{Deserialize, Serialize};
3use sha2::Sha256;
4use thiserror::Error;
5use time::OffsetDateTime;
6
7#[derive(Debug, Error)]
9pub enum SignError {
10 #[error("invalid length of secretkey")]
12 InvalidLength,
13 #[error("urlencoded: {0}")]
15 Urlencoded(#[from] serde_urlencoded::ser::Error),
16}
17
18type HmacSha256 = Hmac<Sha256>;
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct BinanceKey {
23 pub apikey: String,
25 pub secretkey: String,
27}
28
29impl BinanceKey {
30 pub fn sign<T: Serialize>(&self, params: T) -> Result<SignedParams<T>, SignError> {
32 SigningParams::now(params).signed(self)
33 }
34}
35
36#[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 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#[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 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×tamp=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(¶ms)?);
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}