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());
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())
}
}