diff --git a/src/errors.rs b/src/errors.rs index cf55cd6..bcfc700 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -4,6 +4,21 @@ use std::collections::HashMap; use std::fmt; use thiserror::Error; +#[derive(Serialize, Deserialize, Debug, Error)] +pub struct ExmoContentError { + pub code: i16, + pub msg: String, + + #[serde(flatten)] + extra: HashMap, +} + +impl fmt::Display for ExmoContentError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "error code: {} msg: {}", self.code, self.msg) + } +} + #[derive(Serialize, Deserialize, Debug, Error)] pub struct BinanceContentError { pub code: i16, @@ -50,6 +65,8 @@ pub enum OpenLimitsError { #[error(transparent)] CoinbaseError(#[from] CoinbaseContentError), #[error(transparent)] + ExmoError(#[from] ExmoContentError), + #[error(transparent)] NashProtocolError(#[from] nash_protocol::errors::ProtocolError), #[error(transparent)] MissingImplementation(#[from] MissingImplementationContent), diff --git a/src/exmo/client/mod.rs b/src/exmo/client/mod.rs new file mode 100644 index 0000000..10aa93a --- /dev/null +++ b/src/exmo/client/mod.rs @@ -0,0 +1,4 @@ +#[derive(Clone)] +pub struct BaseClient { + pub transport: super::transport::Transport, +} diff --git a/src/exmo/mod.rs b/src/exmo/mod.rs new file mode 100644 index 0000000..8f5cb93 --- /dev/null +++ b/src/exmo/mod.rs @@ -0,0 +1,2 @@ +pub mod model; +mod transport; diff --git a/src/exmo/model/mod.rs b/src/exmo/model/mod.rs new file mode 100644 index 0000000..d0f9d8b --- /dev/null +++ b/src/exmo/model/mod.rs @@ -0,0 +1,421 @@ +pub mod websocket; +use crate::shared::string_to_decimal; +use rust_decimal::prelude::Decimal; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TradeData { + pub trade_id: u64, + #[serde(rename = "type")] + pub event_type: String, + #[serde(with = "string_to_decimal")] + pub price: Decimal, + #[serde(with = "string_to_decimal")] + pub quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, + pub date: u64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OrderBookPosition(#[serde(with = "string_to_decimal")] Decimal); + +// Order book position format is JSON array of 3 numbers: +// ["price","quantity","amount"] +// https://documenter.getpostman.com/view/10287440/SzYXWKPi#308cf5d9-773b-48c0-8e0d-5a1c2ca40751 +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OrderBookData { + pub ask: Vec<[OrderBookPosition; 3]>, + pub bid: Vec<[OrderBookPosition; 3]>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TickerData { + #[serde(with = "string_to_decimal")] + pub buy_price: Decimal, + #[serde(with = "string_to_decimal")] + pub sell_price: Decimal, + #[serde(with = "string_to_decimal")] + pub last_trade: Decimal, + #[serde(with = "string_to_decimal")] + pub high: Decimal, + #[serde(with = "string_to_decimal")] + pub low: Decimal, + #[serde(with = "string_to_decimal")] + pub avg: Decimal, + #[serde(with = "string_to_decimal")] + pub vol: Decimal, + #[serde(with = "string_to_decimal")] + pub vol_curr: Decimal, + pub updated: u64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Settings { + #[serde(with = "string_to_decimal")] + pub min_quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub max_quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub min_price: Decimal, + #[serde(with = "string_to_decimal")] + pub max_price: Decimal, + #[serde(with = "string_to_decimal")] + pub min_amount: Decimal, + #[serde(with = "string_to_decimal")] + pub max_amount: Decimal, + pub price_precision: i32, + #[serde(with = "string_to_decimal")] + pub commission_taker_percent: Decimal, + #[serde(with = "string_to_decimal")] + pub commission_maker_percent: Decimal, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PairSettings(HashMap); + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Currency(Vec); + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CurrencyData { + pub name: String, + pub description: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CurrencyListExtended(Vec); + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct RequiredAmount { + #[serde(with = "string_to_decimal")] + pub quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, + #[serde(with = "string_to_decimal")] + pub avg_price: Decimal, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Candle { + #[serde(rename = "t")] + pub timestamp: u64, + #[serde(rename = "o", with = "string_to_decimal")] + pub open: Decimal, + #[serde(rename = "c", with = "string_to_decimal")] + pub close: Decimal, + #[serde(rename = "h", with = "string_to_decimal")] + pub high: Decimal, + #[serde(rename = "l", with = "string_to_decimal")] + pub low: Decimal, + #[serde(rename = "v", with = "string_to_decimal")] + pub volume: Decimal, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CandlesHistory { + pub candles: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Provider { + #[serde(rename = "type")] + pub provider_method: String, + pub name: String, + pub currency_name: String, + #[serde(with = "string_to_decimal")] + pub min: Decimal, + #[serde(with = "string_to_decimal")] + pub max: Decimal, + pub enabled: bool, + pub comment: String, + pub commission_desc: String, + pub currency_confirmations: i32, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PaymentsProvidersCryptoList(HashMap); + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CurrencyBallance(#[serde(with = "string_to_decimal")] Decimal); + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserInfo { + pub uid: u64, + pub server_date: u64, + pub balances: HashMap, + pub reserved: HashMap, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OrderCreate { + pub result: bool, + pub error: String, + pub order_id: u64, + pub client_id: u64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OrderCancel { + pub result: bool, + pub error: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct StopMarketOrderCreate { + pub client_id: u64, + pub parent_order_id: u64, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ParentOrderData { + pub parent_order_id: String, + pub client_id: String, + pub created: String, + #[serde(rename = "type")] + pub event_type: String, + pub pair: String, + #[serde(with = "string_to_decimal")] + pub quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub trigger_price: Decimal, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserOrderData { + pub order_id: String, + pub client_id: String, + pub created: String, + #[serde(rename = "type")] + pub event_type: String, + pub pair: String, + #[serde(with = "string_to_decimal")] + pub quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub price: Decimal, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserOpenOrders( + HashMap>, + HashMap>, +); + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserBuyDeals { + pub trade_id: u64, + pub client_id: u64, + pub date: u64, + #[serde(rename = "type")] + pub event_type: String, + pub pair: String, + #[serde(with = "string_to_decimal")] + pub quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub price: Decimal, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, + pub order_id: u64, + pub parent_order_id: u64, + pub exec_type: String, + #[serde(with = "string_to_decimal")] + pub commission_amount: Decimal, + pub commission_currency: String, + #[serde(with = "string_to_decimal")] + pub commission_percent: Decimal, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserSellDeals { + pub trade_id: u64, + pub client_id: u64, + pub date: u64, + #[serde(rename = "type")] + pub event_type: String, + pub pair: String, + #[serde(with = "string_to_decimal")] + pub quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub price: Decimal, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, + pub order_id: u64, + pub exec_type: String, + #[serde(with = "string_to_decimal")] + pub commission_amount: Decimal, + pub commission_currency: String, + #[serde(with = "string_to_decimal")] + pub commission_percent: Decimal, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserDeals( + HashMap>, + HashMap>, +); + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserCancelParent { + pub parent_order_id: String, + pub client_id: String, + pub created: String, + #[serde(rename = "type")] + pub event_type: String, + pub pair: String, + #[serde(with = "string_to_decimal")] + pub quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub trigger_price: Decimal, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, + pub reason_status: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserCancelOrder { + pub order_id: String, + pub client_id: String, + pub created: String, + #[serde(rename = "type")] + pub event_type: String, + pub pair: String, + #[serde(with = "string_to_decimal")] + pub quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub price: Decimal, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserCanceledOrders(Vec<(UserCancelParent, UserCancelOrder)>); + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OrderTrades { + #[serde(rename = "type")] + pub event_type: String, + pub in_currency: String, + #[serde(with = "string_to_decimal")] + pub in_amount: Decimal, + pub out_currency: String, + #[serde(with = "string_to_decimal")] + pub out_amount: Decimal, + pub trades: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct DepositAddress(HashMap); + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct WithdrawCrypt { + pub result: bool, + pub error: String, + pub task_id: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct WithdrawGetTxId { + pub result: bool, + pub error: String, + pub status: bool, + pub txid: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ExcodeCreate { + pub result: bool, + pub error: String, + pub task_id: String, + pub code: String, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, + pub currency: String, + pub login: String, + #[serde(with = "string_to_decimal")] + pub commission: Decimal, + pub balances: HashMap, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ExcodeLoad { + pub result: bool, + pub error: String, + pub task_id: String, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, + pub currency: String, + pub reviewing: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct CodeCheck { + pub result: bool, + pub error: String, + pub valid: bool, + pub created: u64, + pub used: bool, + pub used_dt: u64, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, + pub currency: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct WalletHistoryRecord { + pub dt: u64, + #[serde(rename = "type")] + pub event_type: String, + pub curr: String, + pub status: String, + pub provider: String, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, + pub txid: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct WalletHistory { + pub result: bool, + pub error: String, + pub begin: String, + pub end: String, + pub history: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OperationExtra { + pub txid: String, + pub excode: String, + pub invoice: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Operation { + pub operation_id: u64, + pub created: u64, + pub updated: u64, + #[serde(rename = "type")] + pub event_type: String, + pub currency: String, + pub status: String, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, + pub provider: String, + #[serde(with = "string_to_decimal")] + pub commission: Decimal, + pub account: String, + pub order_id: u64, + pub extra: OperationExtra, + pub error: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct WalletOperations { + pub items: Vec, + pub count: i32, +} diff --git a/src/exmo/model/websocket.rs b/src/exmo/model/websocket.rs new file mode 100644 index 0000000..0cbbfdf --- /dev/null +++ b/src/exmo/model/websocket.rs @@ -0,0 +1,291 @@ +use super::{OrderBookData, TickerData, TradeData}; +use crate::shared::string_to_decimal; +use rust_decimal::prelude::Decimal; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ExmoSubscription { + // Public API + Trades(String), // symbol + Ticker(String), // symbol + OrderBookSnapshots(String), // symbol + OrderBookUpdates(String), // symbol + // Authenticated API + UserTrades, + Wallets, + Orders, +} + +#[derive(Debug, Clone, Serialize)] +pub enum ExmoWebsocketMessage { + // General + Greetings(GreetingsMessage), + SubscribtionSuccess(SubscribtionSuccessMessage), + SubscribtionError(SubscribtionErrorMessage), + UnsubscribtionSuccess(UnsubscribtionSuccessMessage), + Maintenance(MaintenanceMessage), + LoginSuccess(AuthSuccessMessage), + Ping, + Pong, + // Public + Trades(TradeMessage), + Ticker(TickerMessage), + OrderBookSnapshots(OrderBookMessage), + OrderBookUpdates(OrderBookMessage), + // Authenticated + UserTrades(UserTradeMessage), + WalletsSnapshot(WalletSnapshotMessage), + WalletsUpdate(WalletUpdateMessage), + OrdersSnapshot(OrderSnapshotMessage), + OrdersUpdate(OrderUpdateMessage), +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct GreetingsMessage { + #[serde(rename = "ts")] + pub timestamp: u64, + pub event: String, + pub code: i32, + pub message: String, + pub session_id: String, +} + +// ??? +// #[derive(Debug, Serialize, Deserialize, Clone)] +// pub struct SubscribeMessage { +// pub id: u64, +// pub method: String, +// pub topics: Vec, +// } + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SubscribtionSuccessMessage { + #[serde(rename = "ts")] + pub timestamp: u64, + pub id: u64, + pub event: String, + pub topic: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct SubscribtionErrorMessage { + #[serde(rename = "ts")] + pub timestamp: u64, + pub id: u64, + pub event: String, + pub code: i32, + pub error: String, +} + +// ??? +// #[derive(Debug, Serialize, Deserialize, Clone)] +// pub struct UnsubscribeMessage { +// pub id: u64, +// pub method: String, +// pub topics: Vec, +// } + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UnsubscribtionSuccessMessage { + #[serde(rename = "ts")] + pub timestamp: u64, + pub id: u64, + pub event: String, + pub topic: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct MaintenanceMessage { + #[serde(rename = "ts")] + pub timestamp: u64, + pub event: String, + pub code: i32, + pub message: String, +} + +// ??? +// #[derive(Debug, Serialize, Deserialize, Clone)] +// pub struct Auth { +// pub method: String, +// pub id: u64, +// pub api_key: String, +// pub sign: String, +// pub nonce: String, +// } + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AuthSuccessMessage { + #[serde(rename = "ts")] + pub timestamp: u64, + pub id: u64, + pub event: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TradeMessage { + #[serde(rename = "ts")] + pub timestamp: u64, + pub event: String, + pub topic: String, + pub data: Vec, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TickerMessage { + #[serde(rename = "ts")] + pub timestamp: u64, + pub event: String, + pub topic: String, + pub data: TickerData, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OrderBookMessage { + #[serde(rename = "ts")] + pub timestamp: u64, + pub event: String, + pub topic: String, + pub data: OrderBookData, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserTradeData { + pub trade_id: u64, + #[serde(rename = "type")] + pub event_type: String, + #[serde(with = "string_to_decimal")] + pub price: Decimal, + #[serde(with = "string_to_decimal")] + pub quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, + pub date: u64, + pub order_id: u64, + pub pair: String, + pub exec_type: String, + #[serde(with = "string_to_decimal")] + pub commission_amount: Decimal, + pub commission_currency: String, + #[serde(with = "string_to_decimal")] + pub commission_percent: Decimal, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct UserTradeMessage { + #[serde(rename = "ts")] + pub timestamp: u64, + pub event: String, + pub topic: String, + pub data: UserTradeData, +} + +// ??? +// TODO: proper deserialization +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct WalletSnapshotData { + pub balances: String, + pub reserved: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct WalletSnapshotMessage { + #[serde(rename = "ts")] + pub timestamp: u64, + pub event: String, + pub topic: String, + pub data: WalletSnapshotData, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct WalletUpdateData { + pub currency: String, + #[serde(with = "string_to_decimal")] + pub ballance: Decimal, + #[serde(with = "string_to_decimal")] + pub reserved: Decimal, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct WalletUpdateMessage { + #[serde(rename = "ts")] + pub timestamp: u64, + pub event: String, + pub topic: String, + pub data: WalletUpdateData, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OrderSnapshotData { + pub order_id: String, + pub client_id: String, + pub pair: String, + #[serde(with = "string_to_decimal")] + pub price: Decimal, + #[serde(with = "string_to_decimal")] + pub quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, + #[serde(with = "string_to_decimal")] + pub original_quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub original_amount: Decimal, + #[serde(rename = "type")] + pub event_type: String, + pub status: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ParentOrderSnapshotData { + pub parent_order_id: String, + pub client_id: String, + pub created: String, + #[serde(rename = "type")] + pub event_type: String, + pub pair: String, + #[serde(with = "string_to_decimal")] + pub quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub trigger_price: Decimal, + #[serde(with = "string_to_decimal")] + pub amount: Decimal, + pub status: String, + #[serde(with = "string_to_decimal")] + pub reserved: Decimal, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OrderSnapshotMessage { + #[serde(rename = "ts")] + pub timestamp: u64, + pub event: String, + pub topic: String, + pub data: Vec<(OrderSnapshotData, ParentOrderSnapshotData)>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OrderUpdateData { + pub order_id: String, + pub client_id: String, + pub pair: String, + #[serde(with = "string_to_decimal")] + pub quantity: Decimal, + #[serde(with = "string_to_decimal")] + pub original_quantity: Decimal, + #[serde(rename = "type")] + pub event_type: String, + pub status: String, + pub last_trade_id: String, + #[serde(with = "string_to_decimal")] + pub last_trade_price: Decimal, + #[serde(with = "string_to_decimal")] + pub last_trade_quantity: Decimal, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OrderUpdateMessage { + #[serde(rename = "ts")] + pub timestamp: u64, + pub event: String, + pub topic: String, + pub data: Vec, +} diff --git a/src/exmo/transport.rs b/src/exmo/transport.rs new file mode 100644 index 0000000..825dd29 --- /dev/null +++ b/src/exmo/transport.rs @@ -0,0 +1,214 @@ +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::Sha512; +use url::Url; + +use crate::{ + errors::{ExmoContentError, OpenLimitsError}, + shared::Result, +}; + +type HmacSha512 = Hmac; + +#[derive(Clone)] +pub struct Transport { + credential: Option<(String, String)>, + client: reqwest::Client, + base_url: String, +} + +impl Transport { + pub fn new(sandbox: bool) -> Result { + let default_headers = Transport::default_headers(None, None); + + let client = reqwest::Client::builder() + .default_headers(default_headers) + .build()?; + + Ok(Transport { + credential: None, + client, + base_url: Transport::get_base_url(sandbox), + }) + } + + pub fn with_credential(api_key: &str, api_secret: &str, sandbox: bool) -> Result { + let default_headers = Transport::default_headers(Some(api_key), Some(api_secret)); + + let client = reqwest::Client::builder() + .default_headers(default_headers) + .build()?; + + Ok(Transport { + client, + credential: Some((api_key.into(), api_secret.into())), + base_url: Transport::get_base_url(sandbox), + }) + } + + // TODO: check if the sandbox endpoint exists at exmo? + fn get_base_url(sandbox: bool) -> String { + if sandbox { + String::from("https://api.exmo.com") + } else { + String::from("https://api.exmo.com") + } + } + + pub fn default_headers( + api_key: Option<&str>, + api_secret: Option<&str>, + ) -> header::HeaderMap { + 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( + "Key", + header::HeaderValue::from_str(key).expect("Couldn't parse API key from string."), + ); + }; + + headers + } + + pub async fn get(&self, endpoint: &str, params: Option<&S>) -> Result + where + O: DeserializeOwned, + S: Serialize, + { + let url = self.get_url(endpoint, params)?; + let request = self.client.get(url).send().await?; + + Ok(self.response_handler(request).await?) + } + + pub async fn post(&self, endpoint: &str, data: Option<&D>) -> Result + where + O: DeserializeOwned, + D: Serialize, + { + let url = self.get_url::<()>(endpoint, None)?; + let request = self.client.post(url).form(&data).send().await?; + + Ok(self.response_handler(request).await?) + } + + pub async fn put(&self, endpoint: &str, data: Option) -> Result + where + O: DeserializeOwned, + D: Serialize, + { + let url = self.get_url::<()>(endpoint, None)?; + let request = self.client.put(url).form(&data).send().await?; + + Ok(self.response_handler(request).await?) + } + + pub async fn delete(&self, endpoint: &str, data: Option<&Q>) -> Result + where + O: DeserializeOwned, + Q: Serialize, + { + let url = self.get_url::<()>(endpoint, None)?; + let request = self.client.delete(url).form(&data).send().await?; + + Ok(self.response_handler(request).await?) + } + + pub async fn signed_get(&self, endpoint: &str, params: Option<&S>) -> Result + where + O: DeserializeOwned, + S: Serialize, + { + let mut url = self.get_url(endpoint, params)?; + + 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 fn get_url(&self, endpoint: &str, params: Option<&Q>) -> Result + 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)); + }; + + 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(&self, url: &Url, body: Option<&D>) -> Result<(&str, String)> + where + D: Serialize, + { + let (key, secret) = self.check_key()?; + // TODO: add nonce!!! + let mut mac = + HmacSha512::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(&self, response: Response) -> Result + where + O: DeserializeOwned, + { + match response.status() { + StatusCode::OK => Ok(response.json::().await?), + StatusCode::INTERNAL_SERVER_ERROR => Err(OpenLimitsError::InternalServerError()), + StatusCode::SERVICE_UNAVAILABLE => Err(OpenLimitsError::ServiceUnavailable()), + StatusCode::UNAUTHORIZED => Err(OpenLimitsError::Unauthorized()), + StatusCode::BAD_REQUEST => { + let error: ExmoContentError = response.json().await?; + + Err(OpenLimitsError::ExmoError(error)) + } + s => Err(OpenLimitsError::UnkownResponse(format!( + "Received response: {:?}", + s + ))), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index e25eb04..2e2a831 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,7 @@ pub mod errors; pub mod exchange; pub mod exchange_info; pub mod exchange_ws; +pub mod exmo; pub mod model; pub mod nash; pub mod shared;