pub mod parse;
use std::collections::BTreeMap;
use std::convert::TryFrom;
use async_trait::async_trait;
use chrono::offset::Utc;
use hex::encode as hex_encode;
use hyper::{body::Buf, Body, Method, Request};
use ring::hmac;
use cxmr_api::Account;
use cxmr_api_clients_errors::Error;
use cxmr_balances::price;
use cxmr_currency::CurrencyPair;
use cxmr_exchanges::{AccountInfo, Exchange, ExchangeOrder, MarketOrder, OrderExecution};
use cxmr_http_client::{send_request, BufExt, HttpsClient};
use self::parse::{order_side_to_str, order_time_to_str, BncAccount, BncOrder, BncUserDataStream};
use crate::public::{parse::order_type_to_str, BINANCE_HOST};
pub type RequestParams = BTreeMap<String, String>;
pub struct PrivateClient {
account: Account,
client: HttpsClient,
signer: hmac::Key,
}
#[async_trait]
impl cxmr_api::PrivateClient for PrivateClient {
type Error = Error;
fn exchange(&self) -> &'static Exchange {
&Exchange::Binance
}
fn account(&self) -> &Account {
&self.account
}
async fn account_info(&self) -> Result<AccountInfo, Self::Error> {
let req = self.build_signed_request(Method::GET, "/api/v3/account", None)?;
let body = send_request(&self.client, req).await?;
let account: BncAccount = ::serde_json::from_reader(body.reader())?;
let mut account = AccountInfo::try_from(account)?;
account.account = self.account().name.clone();
Ok(account)
}
async fn open_orders(&self, pair: CurrencyPair) -> Result<Vec<MarketOrder>, Self::Error> {
let mut params = RequestParams::new();
params.insert("symbol".to_string(), pair.join_no_sep());
params.insert("recvWindow".into(), "60000".to_string());
let req = self.build_signed_request(Method::GET, "/api/v3/openOrders", Some(params))?;
let body = send_request(&self.client, req).await?;
let orders: Vec<BncOrder> = ::serde_json::from_reader(body.reader())?;
orders
.into_iter()
.map(move |order| order.market_order(pair.clone()))
.collect::<Result<Vec<MarketOrder>, _>>()
}
async fn create_order(&self, order: &ExchangeOrder) -> Result<OrderExecution, Self::Error> {
let mut params = RequestParams::new();
params.insert("symbol".to_string(), order.pair.join_no_sep());
params.insert(
"side".to_string(),
order_side_to_str(&order.side).to_string(),
);
params.insert(
"type".to_string(),
order_type_to_str(&order.kind).to_string(),
);
params.insert(
"timeInForce".to_string(),
order_time_to_str(&order.time).to_string(),
);
if let Some(rate) = order.rate {
params.insert("rate".to_string(), price(rate).to_string());
}
params.insert("amount".to_string(), price(order.amount).to_string());
let req = self.build_signed_request(Method::POST, "/api/v1/order", Some(params))?;
let body = send_request(&self.client, req).await?;
let account: BncOrder = ::serde_json::from_reader(body.reader())?;
let account = OrderExecution::try_from(account)?;
Ok(account)
}
async fn user_data_stream(&self, key: Option<String>) -> Result<String, Self::Error> {
match key {
None => self.user_data_stream_new().await,
Some(key) => self.user_data_stream_renew(key).await,
}
}
}
impl PrivateClient {
pub fn new(account: Account, client: HttpsClient) -> Self {
let signer = hmac::Key::new(hmac::HMAC_SHA256, account.credentials.secret.as_bytes());
PrivateClient {
account: account,
client: client,
signer: signer,
}
}
async fn user_data_stream_new(&self) -> Result<String, Error> {
let req = self.build_request(Method::POST, "/api/v1/userDataStream", None)?;
let body = send_request(&self.client, req).await?;
let data: BncUserDataStream = ::serde_json::from_reader(body.reader())?;
Ok(data.into())
}
async fn user_data_stream_renew(&self, key: String) -> Result<String, Error> {
let mut params = RequestParams::new();
params.insert("listenKey".to_string(), key.clone());
let req = self.build_request(Method::PUT, "/api/v1/userDataStream", Some(params))?;
let body = send_request(&self.client, req).await?;
Ok(String::from_utf8_lossy(body.bytes()).to_owned().to_string())
}
fn build_request(
&self,
m: Method,
endpoint: &str,
params: Option<RequestParams>,
) -> Result<Request<Body>, cxmr_http_client::Error> {
let uri = match params {
Some(params) => {
let request = join_params(params);
format!("https://{}{}?{}", BINANCE_HOST, endpoint, request)
}
None => format!("https://{}{}", BINANCE_HOST, endpoint),
};
Request::builder()
.uri(uri)
.method(m)
.header("X-MBX-APIKEY", self.account.credentials.key.clone())
.body(Body::empty())
.map_err(|e| e.into())
}
fn build_signed_request(
&self,
m: Method,
endpoint: &str,
params: Option<RequestParams>,
) -> Result<Request<Body>, cxmr_http_client::Error> {
let uri = self.sign_path(endpoint, params);
Request::builder()
.uri(uri)
.method(m)
.header("X-MBX-APIKEY", self.account.credentials.key.clone())
.body(Body::empty())
.map_err(|e| e.into())
}
fn sign_path(&self, endpoint: &str, params: Option<RequestParams>) -> String {
let mut params = params.unwrap_or_else(|| RequestParams::new());
params.insert(
"timestamp".into(),
Utc::now().timestamp_millis().to_string(),
);
let request = join_params(params);
let signature = hex_encode(hmac::sign(&self.signer, request.as_bytes()).as_ref());
format!(
"https://{}{}?{}&signature={}",
BINANCE_HOST, endpoint, request, signature
)
}
}
impl Clone for PrivateClient {
fn clone(&self) -> Self {
PrivateClient::new(self.account.clone(), self.client.clone())
}
}
fn join_params(params: RequestParams) -> String {
let mut request = String::new();
for (key, value) in ¶ms {
let param = format!("{}={}&", key, value);
request.push_str(param.as_ref());
}
request.pop();
request
}
#[cfg(test)]
mod test {
use super::*;
use cxmr_currency::CurrencyPair;
use cxmr_http_client::build_https_client;
use tokio::runtime::Runtime;
#[test]
fn invalid_credentials_error() {
let http_client = build_https_client().unwrap();
let account = Account::new(
"invalid".to_string(),
"test-secret".to_string(),
Exchange::Binance,
);
let client = PrivateClient::new(account, http_client);
let mut runtime = Runtime::new().unwrap();
let req = cxmr_api::PrivateClient::open_orders(
&client,
CurrencyPair::split_reversed("TEST_PAIR").unwrap(),
);
let resp = runtime.block_on(req);
match resp {
Ok(_) => panic!("unexpected response"),
Err(Error::Http(cxmr_http_client::Error::Response(
cxmr_http_client::ErrorResponse { status, .. },
))) => assert_eq!(status, 401),
Err(e) => panic!("unexpecter error: {:?}", e),
}
}
}