openlimits-binance 0.3.0

Binance implementation for OpenLimits.
Documentation
use chrono::Utc;
use hex::encode as hexify;
use hmac::{Hmac, Mac, NewMac};
use reqwest::header;
use reqwest::Response;
use reqwest::StatusCode;
use serde::de::DeserializeOwned;
use serde::Serialize;
use sha2::Sha256;
use url::Url;
use crate::BinanceContentError;
use openlimits_exchange::OpenLimitsError;
use super::shared::Result;

type HmacSha256 = Hmac<Sha256>;

static RECV_WINDOW: usize = 7000;

#[derive(Clone, Debug)]
pub struct Transport {
    credential: Option<(String, String)>,
    client: reqwest::Client,
    pub recv_window: usize,
    base_url: String,
}

impl Transport {
    pub fn new(sandbox: bool) -> Result<Self> {
        let default_headers = Transport::default_headers(None);
        let client = reqwest::Client::builder()
            .default_headers(default_headers)
            .build()?;

        Ok(Transport {
            credential: None,
            client,
            recv_window: RECV_WINDOW,
            base_url: Transport::get_base_url(sandbox),
        })
    }

    pub fn with_credential(api_key: &str, api_secret: &str, sandbox: bool) -> Result<Self> {
        let default_headers = Transport::default_headers(Some(api_key));
        let client = reqwest::Client::builder()
            .default_headers(default_headers)
            .build()?;

        Ok(Transport {
            client,
            credential: Some((api_key.into(), api_secret.into())),
            recv_window: RECV_WINDOW,
            base_url: Transport::get_base_url(sandbox),
        })
    }

    fn get_base_url(sandbox: bool) -> String {
        if sandbox {
            String::from("https://testnet.binance.vision")
        } else {
            String::from("https://api.binance.com")
        }
    }

    pub fn default_headers(api_key: Option<&str>) -> header::HeaderMap<header::HeaderValue> {
        let mut headers = header::HeaderMap::new();
        headers.insert(
            header::USER_AGENT,
            header::HeaderValue::from_static("open_limit"),
        );
        headers.insert(
            header::CONTENT_TYPE,
            header::HeaderValue::from_static("application/x-www-form-urlencoded"),
        );

        if let Some(key) = api_key {
            headers.insert(
                "X-MBX-APIKEY",
                header::HeaderValue::from_str(key).expect("Couldn't parse API key from string."),
            );
        };

        headers
    }

    pub async fn get<O, S>(&self, endpoint: &str, params: Option<&S>) -> Result<O>
    where
        O: DeserializeOwned,
        S: Serialize,
    {
        let url = self.get_url(endpoint, params, false)?;
        let request = self.client.get(url).send().await?;

        Ok(self.response_handler(request).await?)
    }

    pub async fn post<O, D>(&self, endpoint: &str, data: Option<&D>) -> Result<O>
    where
        O: DeserializeOwned,
        D: Serialize,
    {
        let url = self.get_url::<()>(endpoint, None, false)?;
        let request = self.client.post(url).form(&data).send().await?;

        Ok(self.response_handler(request).await?)
    }

    pub async fn put<O, D>(&self, endpoint: &str, data: Option<D>) -> Result<O>
    where
        O: DeserializeOwned,
        D: Serialize,
    {
        let url = self.get_url::<()>(endpoint, None, false)?;
        let request = self.client.put(url).form(&data).send().await?;

        Ok(self.response_handler(request).await?)
    }

    pub async fn delete<O, Q>(&self, endpoint: &str, data: Option<&Q>) -> Result<O>
    where
        O: DeserializeOwned,
        Q: Serialize,
    {
        let url = self.get_url::<()>(endpoint, None, false)?;
        let request = self.client.delete(url).form(&data).send().await?;

        Ok(self.response_handler(request).await?)
    }

    pub async fn signed_get<O, S>(&self, endpoint: &str, params: Option<&S>) -> Result<O>
    where
        O: DeserializeOwned,
        S: Serialize,
    {
        let mut url = self.get_url(endpoint, params, true)?;

        let (_, signature) = self.signature::<()>(&url, None)?;
        url.query_pairs_mut().append_pair("signature", &signature);

        let request = self.client.get(url).send().await?;

        Ok(self.response_handler(request).await?)
    }

    pub async fn signed_post<D, O>(&self, endpoint: &str, data: Option<&D>) -> Result<O>
    where
        O: DeserializeOwned,
        D: Serialize,
    {
        let mut url = self.get_url::<()>(endpoint, None, true)?;

        let (_, signature) = self.signature(&url, data)?;
        url.query_pairs_mut().append_pair("signature", &signature);

        let request = self.client.post(url).form(&data).send().await?;
        Ok(self.response_handler(request).await?)
    }

    pub async fn signed_put<O, Q>(&self, endpoint: &str, data: Option<&Q>) -> Result<O>
    where
        O: DeserializeOwned,
        Q: Serialize,
    {
        let mut url = self.get_url::<()>(endpoint, None, true)?;

        let (_, signature) = self.signature(&url, data)?;
        url.query_pairs_mut().append_pair("signature", &signature);

        let request = self.client.put(url).form(&data).send().await?;

        Ok(self.response_handler(request).await?)
    }

    pub async fn signed_delete<O, Q>(&self, endpoint: &str, data: Option<&Q>) -> Result<O>
    where
        O: DeserializeOwned,
        Q: Serialize,
    {
        let mut url = self.get_url::<()>(endpoint, None, true)?;

        let (_, signature) = self.signature(&url, data)?;
        url.query_pairs_mut().append_pair("signature", &signature);

        let request = self.client.delete(url).form(&data).send().await?;

        Ok(self.response_handler(request).await?)
    }

    pub fn get_url<Q>(
        &self,
        endpoint: &str,
        params: Option<&Q>,
        add_recv_window: bool,
    ) -> Result<Url>
    where
        Q: Serialize,
    {
        let url = format!("{}{}", self.base_url, endpoint);

        let mut url = Url::parse(&url)?;

        if params.is_some() {
            let query = serde_urlencoded::to_string(params)?;
            url.set_query(Some(&query));
        };

        if add_recv_window {
            url.query_pairs_mut()
                .append_pair("timestamp", &Utc::now().timestamp_millis().to_string());
            url.query_pairs_mut()
                .append_pair("recvWindow", &self.recv_window.to_string());
        };
        Ok(url)
    }

    fn check_key(&self) -> Result<(&str, &str)> {
        match self.credential.as_ref() {
            None => Err(OpenLimitsError::NoApiKeySet()),
            Some((k, s)) => Ok((k, s)),
        }
    }

    pub fn signature<D>(&self, url: &Url, body: Option<&D>) -> Result<(&str, String)>
    where
        D: Serialize,
    {
        let (key, secret) = self.check_key()?;
        let mut mac =
            HmacSha256::new_varkey(secret.as_bytes()).expect("Couldn't construct hmac from bytes.");
        let body = if body.is_some() {
            serde_urlencoded::to_string(body)?
        } else {
            String::from("")
        };

        let sign_message = match url.query() {
            Some(query) => format!("{}{}", query, body),
            None => body,
        };

        mac.update(sign_message.as_bytes());
        let signature = hexify(mac.finalize().into_bytes());
        Ok((key, signature))
    }

    async fn response_handler<O>(&self, response: Response) -> Result<O>
    where
        O: DeserializeOwned,
    {
        match response.status() {
            StatusCode::OK => Ok(response.json::<O>().await?),
            StatusCode::INTERNAL_SERVER_ERROR => Err(OpenLimitsError::InternalServerError()),
            StatusCode::SERVICE_UNAVAILABLE => Err(OpenLimitsError::ServiceUnavailable()),
            StatusCode::UNAUTHORIZED => Err(OpenLimitsError::Unauthorized()),
            StatusCode::BAD_REQUEST => {
                let error: BinanceContentError = response.json().await?;

                Err(OpenLimitsError::Generic(Box::new(error)))
            }
            s => Err(OpenLimitsError::UnkownResponse(format!(
                "Received response: {:?}",
                s
            ))),
        }
    }
}