xapi_binance/common/
signer.rs

1use chrono::Utc;
2use hmac::{Hmac, Mac};
3use redact::Secret;
4use reqwest::Request;
5use serde::Deserialize;
6use sha2::Sha256;
7use xapi_shared::{signer::SharedSignerTrait, ws::error::SharedWsError};
8
9#[derive(Clone, Deserialize)]
10pub struct BnSigner {
11    apikey: String,
12    secret: Secret<String>,
13}
14
15impl SharedSignerTrait for BnSigner {
16    fn sign_request(&self, request: &mut Request) {
17        let url = request.url().clone();
18
19        let query = if let Some(query) = url.query() {
20            query.to_string()
21        } else {
22            "".to_string()
23        };
24
25        let timestamp = Utc::now().timestamp_millis();
26        let mut query_items = query.split("&").map(|x| x.to_string()).collect::<Vec<_>>();
27        query_items.push(format!("timestamp={timestamp}"));
28        query_items.sort();
29
30        let signature = self.sign_query_str(&query_items.join("&"));
31        query_items.push(format!("signature={}", signature));
32
33        request
34            .headers_mut()
35            .insert("X-MBX-APIKEY", self.apikey.as_str().parse().unwrap());
36
37        // TODO sign to body when possible
38
39        request.url_mut().set_query(Some(&query_items.join("&")));
40    }
41}
42
43impl BnSigner {
44    pub fn new(apikey: String, secret: Secret<String>) -> Self {
45        BnSigner { apikey, secret }
46    }
47
48    pub fn sign_ws_payload(
49        &self,
50        payload: Option<serde_json::Value>,
51    ) -> Result<serde_json::Value, SharedWsError> {
52        let mut payload =
53            payload.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));
54
55        let payload_obj = match payload.as_object_mut() {
56            None => {
57                tracing::error!("payload should be an object");
58                return Err(SharedWsError::AppError(
59                    "payload should be an object".to_string(),
60                ));
61            }
62            Some(obj) => obj,
63        };
64
65        payload_obj.insert(
66            "timestamp".to_string(),
67            Utc::now().timestamp_millis().into(),
68        );
69        payload_obj.insert("apiKey".to_string(), self.apikey.clone().into());
70
71        let query_string = serde_urlencoded::to_string(&payload_obj)
72            .inspect_err(|err| {
73                tracing::error!("failed to serialize payload into query string: {err:?}")
74            })
75            .map_err(|err| SharedWsError::SerdeError(err.to_string()))?;
76
77        let signature = self.sign_query_str(&query_string);
78
79        payload_obj.insert("signature".to_string(), signature.into());
80
81        Ok(payload)
82    }
83
84    fn sign_query_str(&self, query: &str) -> String {
85        let mut signed_key = Hmac::<Sha256>::new_from_slice(self.secret.expose_secret().as_bytes())
86            .expect("HMAC can take key of any size");
87        signed_key.update(query.as_bytes());
88        hex::encode(signed_key.finalize().into_bytes())
89    }
90}