Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use generics instead of dynamic dispatch #92

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 39 additions & 21 deletions src/exchanges/binance.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// A module for communicating with the [Binance API](https://binance-docs.github.io/apidocs/spot/en/).

use crate::traits::*;
use generic_api_client::{http::*, websocket::*};
use hmac::{Hmac, Mac};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use sha2::Sha256;
use std::{
str::FromStr,
marker::PhantomData,
time::{SystemTime, Duration},
str::FromStr,
time::{Duration, SystemTime},
};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use generic_api_client::{http::*, websocket::*};
use crate::traits::*;

/// The type returned by [Client::request()].
pub type BinanceRequestResult<T> = Result<T, BinanceRequestError>;
Expand Down Expand Up @@ -146,8 +146,8 @@ pub struct BinanceRequestHandler<'a, R: DeserializeOwned> {
}

/// A `struct` that implements [WebSocketHandler]
pub struct BinanceWebSocketHandler {
message_handler: Box<dyn FnMut(serde_json::Value) + Send>,
pub struct BinanceWebSocketHandler<H: FnMut(serde_json::Value) + Send> {
message_handler: H,
options: BinanceOptions,
}

Expand All @@ -169,11 +169,16 @@ where
config
}

fn build_request(&self, mut builder: RequestBuilder, request_body: &Option<B>, _: u8) -> Result<Request, Self::BuildError> {
fn build_request(
&self,
mut builder: RequestBuilder,
request_body: &Option<B>,
_: u8,
) -> Result<Request, Self::BuildError> {
if let Some(body) = request_body {
let encoded = serde_urlencoded::to_string(body).or(
Err("could not serialize body as application/x-www-form-urlencoded"),
)?;
let encoded = serde_urlencoded::to_string(body).or(Err(
"could not serialize body as application/x-www-form-urlencoded",
))?;
builder = builder
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
.body(encoded);
Expand All @@ -185,7 +190,9 @@ where
builder = builder.header("X-MBX-APIKEY", key);

if self.options.http_auth == BinanceAuth::Sign {
let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); // always after the epoch
let time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap(); // always after the epoch
let timestamp = time.as_millis();

builder = builder.query(&[("timestamp", timestamp)]);
Expand All @@ -195,20 +202,31 @@ where

let mut request = builder.build().or(Err("Failed to build request"))?;
let query = request.url().query().unwrap(); // we added the timestamp query
let body = request.body().and_then(|body| body.as_bytes()).unwrap_or_default();
let body = request
.body()
.and_then(|body| body.as_bytes())
.unwrap_or_default();

hmac.update(&[query.as_bytes(), body].concat());
let signature = hex::encode(hmac.finalize().into_bytes());

request.url_mut().query_pairs_mut().append_pair("signature", &signature);
request
.url_mut()
.query_pairs_mut()
.append_pair("signature", &signature);

return Ok(request);
}
}
builder.build().or(Err("failed to build request"))
}

fn handle_response(&self, status: StatusCode, headers: HeaderMap, response_body: Bytes) -> Result<Self::Successful, Self::Unsuccessful> {
fn handle_response(
&self,
status: StatusCode,
headers: HeaderMap,
response_body: Bytes,
) -> Result<Self::Successful, Self::Unsuccessful> {
if status.is_success() {
serde_json::from_slice(&response_body).map_err(|error| {
log::debug!("Failed to parse response due to an error: {}", error);
Expand Down Expand Up @@ -247,7 +265,7 @@ where
}
}

impl WebSocketHandler for BinanceWebSocketHandler {
impl<H: FnMut(serde_json::Value) + Send + 'static> WebSocketHandler for BinanceWebSocketHandler<H> {
fn websocket_config(&self) -> WebSocketConfig {
let mut config = self.options.websocket_config.clone();
if self.options.websocket_url != BinanceWebSocketUrl::None {
Expand All @@ -264,7 +282,7 @@ impl WebSocketHandler for BinanceWebSocketHandler {
} else {
log::debug!("Invalid JSON message received");
}
},
}
WebSocketMessage::Binary(_) => log::debug!("Unexpected binary message received"),
WebSocketMessage::Ping(_) | WebSocketMessage::Pong(_) => (),
}
Expand Down Expand Up @@ -366,12 +384,12 @@ where
}

impl<H: FnMut(serde_json::Value) + Send + 'static> WebSocketOption<H> for BinanceOption {
type WebSocketHandler = BinanceWebSocketHandler;
type WebSocketHandler = BinanceWebSocketHandler<H>;

#[inline(always)]
fn websocket_handler(handler: H, options: Self::Options) -> Self::WebSocketHandler {
BinanceWebSocketHandler {
message_handler: Box::new(handler),
message_handler: handler,
options,
}
}
Expand Down
106 changes: 68 additions & 38 deletions src/exchanges/bitflyer.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
//! A module for communicating with the [bitFlyer API](https://lightning.bitflyer.com/docs).
//! For example usages, see files in the examples/ directory.

use std::{
marker::PhantomData,
time::SystemTime,
use crate::traits::*;
use generic_api_client::{
http::{header::HeaderValue, *},
websocket::*,
};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use rand::{Rng, distributions::Alphanumeric};
use rand::{distributions::Alphanumeric, Rng};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::json;
use generic_api_client::{http::{*, header::HeaderValue}, websocket::*};
use crate::traits::*;
use sha2::Sha256;
use std::{marker::PhantomData, time::SystemTime};

/// The type returned by [Client::request()].
pub type BitFlyerRequestResult<T> = Result<T, BitFlyerRequestError>;
Expand Down Expand Up @@ -105,8 +105,8 @@ pub struct BitFlyerRequestHandler<'a, R: DeserializeOwned> {
}

/// A `struct` that implements [WebSocketHandler]
pub struct BitFlyerWebSocketHandler {
message_handler: Box<dyn FnMut(BitFlyerChannelMessage) + Send>,
pub struct BitFlyerWebSocketHandler<H: FnMut(BitFlyerChannelMessage) + Send> {
message_handler: H,
auth_id: Option<String>,
options: BitFlyerOptions,
}
Expand All @@ -128,9 +128,15 @@ where
config
}

fn build_request(&self, mut builder: RequestBuilder, request_body: &Option<B>, _: u8) -> Result<Request, Self::BuildError> {
fn build_request(
&self,
mut builder: RequestBuilder,
request_body: &Option<B>,
_: u8,
) -> Result<Request, Self::BuildError> {
if let Some(body) = request_body {
let json = serde_json::to_vec(body).or(Err("could not serialize body as application/json"))?;
let json =
serde_json::to_vec(body).or(Err("could not serialize body as application/json"))?;
builder = builder
.header(header::CONTENT_TYPE, "application/json")
.body(json);
Expand All @@ -140,15 +146,18 @@ where

if self.options.http_auth {
// https://lightning.bitflyer.com/docs?lang=en#authentication
let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); // always after the epoch
let time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap(); // always after the epoch
let timestamp = time.as_millis() as u64;

let mut path = request.url().path().to_owned();
if let Some(query) = request.url().query() {
path.push('?');
path.push_str(query)
}
let body = request.body()
let body = request
.body()
.and_then(|body| body.as_bytes())
.map(String::from_utf8_lossy)
.unwrap_or_default();
Expand All @@ -161,20 +170,27 @@ where
hmac.update(sign_contents.as_bytes());
let signature = hex::encode(hmac.finalize().into_bytes());

let key = HeaderValue::from_str(self.options.key.as_deref().ok_or("API key not set")?).or(
Err("invalid character in API key")
)?;
let key = HeaderValue::from_str(self.options.key.as_deref().ok_or("API key not set")?)
.or(Err("invalid character in API key"))?;
let headers = request.headers_mut();
headers.insert("ACCESS-KEY", key);
headers.insert("ACCESS-TIMESTAMP", HeaderValue::from(timestamp));
headers.insert("ACCESS-SIGN", HeaderValue::from_str(&signature).unwrap()); // hex digits are valid
headers.insert(header::CONTENT_TYPE, HeaderValue::from_str("application/json").unwrap()); // only contains valid letters
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_str("application/json").unwrap(),
); // only contains valid letters
}

Ok(request)
}

fn handle_response(&self, status: StatusCode, _: HeaderMap, response_body: Bytes) -> Result<Self::Successful, Self::Unsuccessful> {
fn handle_response(
&self,
status: StatusCode,
_: HeaderMap,
response_body: Bytes,
) -> Result<Self::Successful, Self::Unsuccessful> {
if status.is_success() {
serde_json::from_slice(&response_body).map_err(|error| {
log::debug!("Failed to parse response due to an error: {}", error);
Expand All @@ -193,7 +209,9 @@ where
}
}

impl WebSocketHandler for BitFlyerWebSocketHandler {
impl<H: FnMut(BitFlyerChannelMessage) + Send + 'static> WebSocketHandler
for BitFlyerWebSocketHandler<H>
{
fn websocket_config(&self) -> WebSocketConfig {
let mut config = self.options.websocket_config.clone();
if self.options.websocket_url != BitFlyerWebSocketUrl::None {
Expand All @@ -207,7 +225,9 @@ impl WebSocketHandler for BitFlyerWebSocketHandler {
// https://bf-lightning-api.readme.io/docs/realtime-api-auth
if let Some(key) = self.options.key.as_deref() {
if let Some(secret) = self.options.secret.as_deref() {
let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); // always after the epoch
let time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap(); // always after the epoch
let timestamp = time.as_millis() as u64;
let nonce: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
Expand All @@ -223,16 +243,19 @@ impl WebSocketHandler for BitFlyerWebSocketHandler {
let id = format!("_auth{}", time.as_nanos());
self.auth_id = Some(id.clone());

return vec![WebSocketMessage::Text(json!({
"method": "auth",
"params": {
"api_key": key,
"timestamp": timestamp,
"nonce": nonce,
"signature": signature,
},
"id": id,
}).to_string())];
return vec![WebSocketMessage::Text(
json!({
"method": "auth",
"params": {
"api_key": key,
"timestamp": timestamp,
"nonce": nonce,
"signature": signature,
},
"id": id,
})
.to_string(),
)];
} else {
log::debug!("API secret not set.");
};
Expand Down Expand Up @@ -261,7 +284,7 @@ impl WebSocketHandler for BitFlyerWebSocketHandler {
Err(_) => {
log::debug!("Invalid JSON-RPC message received");
return vec![];
},
}
};
if self.options.websocket_auth && self.auth_id == message.id {
// result of auth
Expand All @@ -277,20 +300,27 @@ impl WebSocketHandler for BitFlyerWebSocketHandler {
(self.message_handler)(channel_message);
}
}
},
}
WebSocketMessage::Binary(_) => log::debug!("Unexpected binary message received"),
WebSocketMessage::Ping(_) | WebSocketMessage::Pong(_) => (),
}
vec![]
}
}

impl BitFlyerWebSocketHandler {
impl<H: FnMut(BitFlyerChannelMessage) + Send> BitFlyerWebSocketHandler<H> {
#[inline]
fn message_subscribe(&self) -> Vec<WebSocketMessage> {
self.options.websocket_channels.clone().into_iter().map(|channel| {
WebSocketMessage::Text(json!({ "method": "subscribe", "params": { "channel": channel } }).to_string())
}).collect()
self.options
.websocket_channels
.clone()
.into_iter()
.map(|channel| {
WebSocketMessage::Text(
json!({ "method": "subscribe", "params": { "channel": channel } }).to_string(),
)
})
.collect()
}
}

Expand Down Expand Up @@ -370,12 +400,12 @@ where
}

impl<H: FnMut(BitFlyerChannelMessage) + Send + 'static> WebSocketOption<H> for BitFlyerOption {
type WebSocketHandler = BitFlyerWebSocketHandler;
type WebSocketHandler = BitFlyerWebSocketHandler<H>;

#[inline(always)]
fn websocket_handler(handler: H, options: Self::Options) -> Self::WebSocketHandler {
BitFlyerWebSocketHandler {
message_handler: Box::new(handler),
message_handler: handler,
auth_id: None,
options,
}
Expand Down
Loading