Skip to content

Commit

Permalink
fix signing; fix some invalid parsing;
Browse files Browse the repository at this point in the history
  • Loading branch information
royroyisroy committed May 3, 2024
1 parent 92622a7 commit 6a82b42
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 246 deletions.
9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,23 @@ tokio = { version = "1.0.0", features = ["rt", "rt-multi-thread", "macros"], opt
simd-json = { version = "0.13.4", features = ["runtime-detection", "known-key"], optional = true }
tungstenite = { version = "0.20.1", features = ["native-tls"], optional = true }
log = { version = "0.4.20", optional = true }
dotenv = { version = "0.15.0", optional = true }
env_logger = { version = "0.10.0", features = [], optional = true }
rust_decimal = { version = "1" , optional = true }

# FIXME: to be removed
futures-util = "0.3.28"

# FIXME: should be feature-gated
reqwest = { version = "0.11.22", features = ["json"] }
reqwest = { version = "0.11.22", features = ["json", "blocking"] }
chrono = "0.4.38"

[dev-dependencies]
dotenv = { version = "0.15.0" }

[features]
default = ["dep:log"]
vip = []
simd = ["dep:simd-json"]
websocket = ["dep:tungstenite"]
example = ["dep:dotenv", "dep:env_logger", "dep:tokio", "websocket"]
example = ["dep:env_logger", "dep:tokio", "websocket"]
orderbook = ["dep:rust_decimal"]
8 changes: 3 additions & 5 deletions examples/rest_get_instruments.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
use okx_rs::api::v5::model::InstrumentType::Spot;
use okx_rs::api::v5::GetInstruments;
use okx_rs::api::{Options, Production, Rest};
use okx_rs::api::{blocking, Options, Production};

#[tokio::main]
async fn main() {
fn main() {
env_logger::try_init().unwrap();

let client = Rest::new(Options::new(Production));
let client = blocking::Rest::new(Options::new(Production));
let response = client
.request(GetInstruments {
inst_type: Spot,
uly: None,
inst_family: None,
inst_id: None,
})
.await
.unwrap();
println!("{}", serde_json::to_string_pretty(&response).unwrap());
}
13 changes: 2 additions & 11 deletions examples/ws_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,15 @@ fn main() {
let key = std::env::var("OKX_API_KEY").unwrap();
let secret = std::env::var("OKX_API_SECRET").unwrap();
let passphrase = std::env::var("OKX_API_PASSPHRASE").unwrap();
let options = Options {
env: DemoTrading,
key: Some(key),
secret: Some(secret),
passphrase: Some(passphrase),
};
let options = Options::new_with(DemoTrading, key, secret, passphrase);

let (mut client, response) = tungstenite::connect(DemoTrading.private_websocket()).unwrap();
println!("Connected to the server");
println!("Response HTTP code: {}", response.status());
println!("Response contains the following headers:");
println!("{:?}", response.headers());

let auth_msg = OKXAuth::ws_auth(options).unwrap();
client.send(auth_msg.into()).unwrap();

let auth_resp = client.read().unwrap();
info!("{:?}", auth_resp);
info!("auth_resp: {:?}", auth_resp);

client
.send(AccountChannel.subscribe_message().into())
Expand Down
177 changes: 177 additions & 0 deletions src/api/blocking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
use std::{str::FromStr, time::Duration};

use chrono::Utc;
use reqwest::{
blocking::{Client, ClientBuilder},
header::{HeaderMap, HeaderName, HeaderValue},
Method,
};
use url::Url;

use crate::api::{
credential::Credential,
error::{ApiError, Error},
v5::ApiResponse,
};

use super::{v5::Request, Options};

// FIXME: mostly a copy paste from async variant
#[derive(Clone)]
pub struct Rest {
options: Options,
client: Client,
}

impl Rest {
pub fn new(options: Options) -> Self {
let mut headers = HeaderMap::new();

let client = ClientBuilder::new()
.default_headers(headers)
.tcp_nodelay(true)
.tcp_keepalive(Duration::from_secs(30))
.timeout(Duration::from_secs(30))
.build()
.unwrap();

Self { client, options }
}

#[inline]
pub fn options(&self) -> &Options {
&self.options
}

#[inline]
pub fn request<R>(&self, req: R) -> crate::api::error::Result<R::Response>
where
R: Request,
{
let mut callback = || {};
self.request_with(req, &mut callback)
}

pub fn request_with<R>(
&self,
req: R,
on_send: &mut (dyn FnMut() + Sync + Send),
) -> crate::api::error::Result<R::Response>
where
R: Request,
{
let (params, body) = match R::METHOD {
Method::GET => (Some(serde_qs::to_string(&req)?), String::new()),
_ => (None, serde_json::to_string(&req)?),
};
let mut path = req.path().into_owned();
if let Some(params) = params {
if !params.is_empty() {
path.push('?');
path.push_str(&params);
}
}
let url = format!("{}{}", self.options().rest(), path);
log::debug!("{} {}", url, body);
// example: 2020-12-08T09:08:57.715Z
let timestamp = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();

// TODO: reuse headers if possible
let mut headers = HeaderMap::new();
headers.insert(
reqwest::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);

if R::AUTH {
let passphrase = self
.options()
.passphrase
.to_owned()
.ok_or(Error::NoSecretConfigured)?;
let credential: Credential = match self.options().try_into() {
Ok(credential) => credential,
Err(_) => return Err(Error::NoSecretConfigured),
};

let (key, signature) =
credential.signature(R::METHOD, &timestamp, &Url::from_str(&url).unwrap(), &body);

headers.insert(
HeaderName::from_str("OK-ACCESS-KEY").unwrap(),
HeaderValue::from_str(key).unwrap(),
);
headers.insert(
HeaderName::from_str("OK-ACCESS-SIGN").unwrap(),
HeaderValue::from_str(&signature).unwrap(),
);
headers.insert(
HeaderName::from_str("OK-ACCESS-TIMESTAMP").unwrap(),
HeaderValue::from_str(&timestamp).unwrap(),
);
headers.insert(
HeaderName::from_str("OK-ACCESS-PASSPHRASE").unwrap(),
HeaderValue::from_str(&passphrase).unwrap(),
);
}

if let Some(extras) = self.options.env.headers() {
for (key, val) in extras {
headers.insert(
HeaderName::from_str(key).unwrap(),
HeaderValue::from_str(val).unwrap(),
);
}
}

let sent = match self
.client
.request(R::METHOD, &url)
.headers(headers)
.body(body)
.send()
{
Ok(sent) => sent,
Err(err) => {
log::error!("{err}");
return Err(Error::Reqwest(err));
}
};

if let Err(err) = sent.error_for_status_ref() {
return Err(Error::Reqwest(err));
}
on_send();

let body = sent.bytes()?;

// println!("{}", std::str::from_utf8(body.as_ref()).unwrap()); // DEBUG

match serde_json::from_slice::<ApiResponse<R::Response>>(&body) {
Ok(ApiResponse { code, msg, data }) => match code {
Some(0) => {
if let Some(data) = data {
Ok(data)
} else {
Err(Error::Api(ApiError {
code,
msg: Some("Success but empty response".to_owned()),
data: None,
conn_id: None,
}))
}
}
code => Err(Error::Api(ApiError {
code,
msg,
data,
conn_id: None,
})),
},
Err(e) => {
log::error!("{}", String::from_utf8_lossy(&body));
Err(Error::Json(e))
}
}
}
}
7 changes: 5 additions & 2 deletions src/api/credential.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::Options;
use anyhow::{bail, ensure};
use base64::encode;
use hmac::{Hmac, Mac};
use reqwest::{Method, Url};
Expand Down Expand Up @@ -79,16 +80,18 @@ impl Credential {
}

impl TryFrom<&Options> for Credential {
type Error = &'static str;
type Error = anyhow::Error;

fn try_from(options: &Options) -> Result<Self, Self::Error> {
ensure!(options.key.is_some(), "key is not set");
ensure!(options.secret.is_some(), "secret is not set");
if let (Some(key), Some(secret)) = (&options.key, &options.secret) {
Ok(Self {
key: key.to_owned(),
secret: secret.to_owned(),
})
} else {
Err("not enough credentials from Options")
bail!("not enough credentials from Options")
}
}
}
44 changes: 13 additions & 31 deletions src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::api::credential::Credential;
use crate::api::error::Error;
use crate::api::v5::{ApiResponse, Request};
use chrono::Utc;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use reqwest::{Client, ClientBuilder, Method, Url};
use std::convert::TryInto;
Expand All @@ -14,6 +15,7 @@ mod options;
pub mod credential;
pub mod error;
pub use self::options::*;
pub mod blocking;
pub mod v5;

#[derive(Clone)]
Expand All @@ -26,13 +28,6 @@ impl Rest {
pub fn new(options: Options) -> Self {
let mut headers = HeaderMap::new();

if let Some(key) = &options.key {
headers.insert(
HeaderName::from_str("OK-ACCESS-KEY").unwrap(),
HeaderValue::from_str(key).unwrap(),
);
}

let client = ClientBuilder::new()
.default_headers(headers)
.tcp_nodelay(true)
Expand Down Expand Up @@ -79,12 +74,8 @@ impl Rest {
}
let url = format!("{}{}", self.options().rest(), path);
log::debug!("{} {}", url, body);
let now = std::time::SystemTime::now();
let timestamp = now
.duration_since(std::time::UNIX_EPOCH)
.expect("Time went backwards")
.as_millis()
.to_string();
// example: 2020-12-08T09:08:57.715Z
let timestamp = Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string();

// TODO: reuse headers if possible
let mut headers = HeaderMap::new();
Expand Down Expand Up @@ -125,6 +116,15 @@ impl Rest {
);
}

if let Some(extras) = self.options.env.headers() {
for (key, val) in extras {
headers.insert(
HeaderName::from_str(key).unwrap(),
HeaderValue::from_str(val).unwrap(),
);
}
}

let sent = match self
.client
.request(R::METHOD, &url)
Expand Down Expand Up @@ -177,21 +177,3 @@ impl Rest {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

use serde::Serialize;
use serde_json::Value;

#[derive(Debug, Clone, Serialize, Default)]
pub struct GetAccountBalance {}

impl Request for GetAccountBalance {
const METHOD: Method = Method::GET;
const PATH: &'static str = "/asset/balances";
const AUTH: bool = true;
type Response = Value;
}
}
Loading

0 comments on commit 6a82b42

Please sign in to comment.