xapi-binance 0.0.1

Binance API client
Documentation
use chrono::Utc;
use hmac::{Hmac, Mac};
use redact::Secret;
use reqwest::Request;
use serde::Deserialize;
use sha2::Sha256;
use xapi_shared::{signer::SharedSignerTrait, ws::error::SharedWsError};

#[derive(Clone, Deserialize)]
pub struct BnSigner {
    apikey: String,
    secret: Secret<String>,
}

impl SharedSignerTrait for BnSigner {
    fn sign_request(&self, request: &mut Request) {
        let url = request.url().clone();

        let query = if let Some(query) = url.query() {
            query.to_string()
        } else {
            "".to_string()
        };

        let timestamp = Utc::now().timestamp_millis();
        let mut query_items = query.split("&").map(|x| x.to_string()).collect::<Vec<_>>();
        query_items.push(format!("timestamp={timestamp}"));
        query_items.sort();

        let signature = self.sign_query_str(&query_items.join("&"));
        query_items.push(format!("signature={}", signature));

        request
            .headers_mut()
            .insert("X-MBX-APIKEY", self.apikey.as_str().parse().unwrap());

        // TODO sign to body when possible

        request.url_mut().set_query(Some(&query_items.join("&")));
    }
}

impl BnSigner {
    pub fn new(apikey: String, secret: Secret<String>) -> Self {
        BnSigner { apikey, secret }
    }

    pub fn sign_ws_payload(
        &self,
        payload: Option<serde_json::Value>,
    ) -> Result<serde_json::Value, SharedWsError> {
        let mut payload =
            payload.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::new()));

        let payload_obj = match payload.as_object_mut() {
            None => {
                tracing::error!("payload should be an object");
                return Err(SharedWsError::AppError(
                    "payload should be an object".to_string(),
                ));
            }
            Some(obj) => obj,
        };

        payload_obj.insert(
            "timestamp".to_string(),
            Utc::now().timestamp_millis().into(),
        );
        payload_obj.insert("apiKey".to_string(), self.apikey.clone().into());

        let query_string = serde_urlencoded::to_string(&payload_obj)
            .inspect_err(|err| {
                tracing::error!("failed to serialize payload into query string: {err:?}")
            })
            .map_err(|err| SharedWsError::SerdeError(err.to_string()))?;

        let signature = self.sign_query_str(&query_string);

        payload_obj.insert("signature".to_string(), signature.into());

        Ok(payload)
    }

    fn sign_query_str(&self, query: &str) -> String {
        let mut signed_key = Hmac::<Sha256>::new_from_slice(self.secret.expose_secret().as_bytes())
            .expect("HMAC can take key of any size");
        signed_key.update(query.as_bytes());
        hex::encode(signed_key.finalize().into_bytes())
    }
}