use std::{
borrow::Cow,
marker::PhantomData,
time::{Duration, SystemTime},
vec,
};
use ahash::AHashSet;
use eyre::{WrapErr as _, eyre};
use generics::{ConstructAuthError, UrlError, tokio_tungstenite::tungstenite};
use hmac::{Hmac, KeyInit as _, Mac};
use jiff::Timestamp;
use secrecy::{ExposeSecret as _, SecretString};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::json;
use sha2::Sha256;
use tracing::instrument;
use url::Url;
use v_exchanges_api_generics::{
http::{header::HeaderValue, *},
ws::*,
};
use crate::traits::*;
#[derive(Debug, Default)]
pub enum BybitOption {
#[default]
None,
Pubkey(String),
Secret(SecretString),
Testnet(bool),
HttpUrl(BybitHttpUrl),
HttpAuth(BybitHttpAuth),
RecvWindow(std::time::Duration),
WsUrl(BybitWsUrlBase),
WsAuth(bool),
WsConfig(WsConfig),
WsTopics(Vec<String>),
}
#[derive(Clone, derive_more::Debug, smart_default::SmartDefault)]
pub struct BybitOptions {
pub pubkey: Option<String>,
#[debug("[REDACTED]")]
pub secret: Option<SecretString>,
pub testnet: bool,
pub http_url: BybitHttpUrl,
pub http_auth: BybitHttpAuth,
pub recv_window: Option<std::time::Duration>,
pub ws_url: BybitWsUrlBase,
pub ws_auth: bool,
#[default(bybit_ws_config())]
pub ws_config: WsConfig,
pub ws_topics: AHashSet<String>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum BybitHttpUrl {
#[default]
Bybit,
Bytick,
None,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum BybitHttpAuth {
SpotV1,
BelowV3,
UsdcContractV1,
V3AndAbove,
#[default]
None,
}
#[non_exhaustive]
#[derive(Debug)]
pub enum BybitHandlerError {
ApiError(serde_json::Value),
IpBan(serde_json::Value),
ParseError,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
#[serde(from = "i32", into = "i32")]
pub enum BybitErrorCode {
InvalidApiKey(i32),
ErrorSign(i32),
PermissionDenied(i32),
TooManyVisits(i32),
AuthenticationFailed(i32),
IpBanned(i32),
UnmatchedIp(i32),
InvalidDuplicateRequest(i32),
ServerError(i32),
RouteNotFound(i32),
IpRateLimit(i32),
ComplianceRules(i32),
ApiKeyExpired(i32),
OrderNotExist(i32),
InsufficientBalance(i32),
#[default]
Ok,
Other(i32),
}
impl BybitErrorCode {
fn as_i32(self) -> i32 {
match self {
Self::Ok => 0,
Self::InvalidApiKey(c)
| Self::ErrorSign(c)
| Self::PermissionDenied(c)
| Self::TooManyVisits(c)
| Self::AuthenticationFailed(c)
| Self::IpBanned(c)
| Self::UnmatchedIp(c)
| Self::InvalidDuplicateRequest(c)
| Self::ServerError(c)
| Self::RouteNotFound(c)
| Self::IpRateLimit(c)
| Self::ComplianceRules(c)
| Self::ApiKeyExpired(c)
| Self::OrderNotExist(c)
| Self::InsufficientBalance(c)
| Self::Other(c) => c,
}
}
}
pub struct BybitRequestHandler<'a, R: DeserializeOwned> {
options: BybitOptions,
_phantom: PhantomData<&'a R>,
}
impl<R> BybitRequestHandler<'_, R>
where
R: DeserializeOwned,
{
fn v1_auth<B>(
builder: RequestBuilder,
request_body: &Option<B>,
key: &str,
timestamp: u128,
mut hmac: Hmac<Sha256>,
spot: bool,
window: Option<std::time::Duration>,
) -> Result<Request, BuildError>
where
B: Serialize, {
fn sort_and_add<'a>(mut pairs: Vec<(Cow<str>, Cow<'a, str>)>, key: &'a str, timestamp: u128) -> String {
pairs.push((Cow::Borrowed("api_key"), Cow::Borrowed(key)));
pairs.push((Cow::Borrowed("timestamp"), Cow::Owned(timestamp.to_string())));
pairs.sort_unstable();
let mut urlencoded = String::default();
for (key, value) in pairs {
urlencoded.push_str(&key);
if !value.is_empty() {
urlencoded.push('=');
urlencoded.push_str(&value);
}
urlencoded.push('&');
}
urlencoded.pop(); urlencoded
}
let mut request = builder.build().expect("My understanding is client can't trigger this. So fail fast for dev");
if matches!(*request.method(), Method::GET | Method::DELETE) {
let mut queries: Vec<_> = request.url().query_pairs().collect();
if let Some(window) = window {
let window_ms = window.as_millis() as u64;
if spot {
queries.push((Cow::Borrowed("recvWindow"), Cow::Owned(window_ms.to_string())));
} else {
queries.push((Cow::Borrowed("recv_window"), Cow::Owned(window_ms.to_string())));
}
}
let query = sort_and_add(queries, key, timestamp);
request.url_mut().set_query(Some(&query));
hmac.update(query.as_bytes());
let signature = hex::encode(hmac.finalize().into_bytes());
request.url_mut().query_pairs_mut().append_pair("sign", &signature);
if let Some(body) = request_body {
if spot {
let body_string = serde_urlencoded::to_string(body)?;
*request.body_mut() = Some(body_string.into());
request.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"));
} else {
let body_string = serde_json::to_string(body)?;
*request.body_mut() = Some(body_string.into());
request.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("application/json"));
}
}
} else {
let mut body = if let Some(body) = request_body {
serde_urlencoded::to_string(body)?
} else {
String::default()
};
if let Some(window) = window {
let window_ms = window.as_millis() as u64;
if !body.is_empty() {
body.push('&');
}
if spot {
body.push_str("recvWindow=");
} else {
body.push_str("recv_window=");
}
body.push_str(&window_ms.to_string());
}
let pairs: Vec<_> = body
.split('&')
.map(|pair| pair.split_once('=').unwrap_or((pair, "")))
.map(|(k, v)| (Cow::Borrowed(k), Cow::Borrowed(v)))
.collect();
let mut sorted_query_string = sort_and_add(pairs, key, timestamp);
hmac.update(sorted_query_string.as_bytes());
let signature = hex::encode(hmac.finalize().into_bytes());
sorted_query_string.push_str(&format!("&sign={signature}"));
if spot {
*request.body_mut() = Some(sorted_query_string.into());
request.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("application/x-www-form-urlencoded"));
} else {
let body: serde_json::Value = serde_urlencoded::from_str(&sorted_query_string).unwrap(); *request.body_mut() = Some(body.to_string().into());
request.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("application/json"));
}
}
Ok(request)
}
fn v3_auth<B>(
mut builder: RequestBuilder,
request_body: &Option<B>,
key: &str,
timestamp: u128,
mut hmac: Hmac<Sha256>,
version_header: bool,
window: Option<std::time::Duration>,
) -> Result<Request, BuildError>
where
B: Serialize, {
let body = if let Some(body) = request_body {
let json = serde_json::to_value(body)?;
builder = builder.header(header::CONTENT_TYPE, "application/json").body(json.to_string());
Some(json)
} else {
None
};
let mut request = builder.build().expect("My understanding is client can't trigger this. So fail fast for dev");
let mut sign_contents = format!("{timestamp}{key}");
if let Some(window) = window {
let window_ms = window.as_millis() as u64;
sign_contents.push_str(&window_ms.to_string());
}
if matches!(*request.method(), Method::GET | Method::DELETE) {
if let Some(query) = request.url().query() {
sign_contents.push_str(query);
}
} else {
let body = body.unwrap_or_else(|| {
*request.body_mut() = Some("{}".into());
request.headers_mut().insert(header::CONTENT_TYPE, HeaderValue::from_static("application/json"));
json!({})
});
sign_contents.push_str(&body.to_string());
}
hmac.update(sign_contents.as_bytes());
let signature = hex::encode(hmac.finalize().into_bytes());
let headers = request.headers_mut();
if version_header {
headers.insert("X-BAPI-SIGN-TYPE", HeaderValue::from(2));
}
headers.insert("X-BAPI-SIGN", HeaderValue::from_str(&signature).unwrap()); headers.insert(
"X-BAPI-API-KEY",
HeaderValue::from_str(key).or(Err(ConstructAuthError::new_invalid_character_in_api_key(key.to_owned())))?,
);
headers.insert("X-BAPI-TIMESTAMP", HeaderValue::from(timestamp as u64));
if let Some(window) = window {
let window_ms = window.as_millis() as u64;
headers.insert("X-BAPI-RECV-WINDOW", HeaderValue::from(window_ms));
}
Ok(request)
}
}
#[derive(Debug, derive_new::new)]
pub struct BybitWsHandler {
options: BybitOptions,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum BybitWsUrlBase {
#[default]
Bybit,
Bytick,
None,
}
fn bybit_ws_config() -> WsConfig {
let mut config = WsConfig::default();
config.set_active_ping_freq(Duration::from_secs(20)).expect("non-zero literal");
config
}
impl EndpointUrl for BybitHttpUrl {
fn url_mainnet(&self) -> Url {
match self {
Self::Bybit => Url::parse("https://api.bybit.com").unwrap(),
Self::Bytick => Url::parse("https://api.bytick.com").unwrap(),
Self::None => Url::parse("").unwrap(),
}
}
fn url_testnet(&self) -> Option<Url> {
match self {
Self::Bybit => Some(Url::parse("https://api-testnet.bybit.com").unwrap()),
Self::Bytick => None, Self::None => Some(Url::parse("").unwrap()),
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
struct BybitError {
code: BybitErrorCode,
msg: String,
}
impl From<BybitError> for ApiError {
fn from(e: BybitError) -> Self {
use v_exchanges_api_generics::http::AuthError;
match e.code {
BybitErrorCode::ApiKeyExpired(_) => AuthError::KeyExpired { msg: e.msg }.into(),
BybitErrorCode::InvalidApiKey(_) | BybitErrorCode::ErrorSign(_) | BybitErrorCode::AuthenticationFailed(_) | BybitErrorCode::UnmatchedIp(_) =>
AuthError::Unauthorized { msg: e.msg }.into(),
BybitErrorCode::PermissionDenied(_) => AuthError::Unauthorized { msg: e.msg }.into(),
BybitErrorCode::TooManyVisits(_) | BybitErrorCode::IpBanned(_) | BybitErrorCode::IpRateLimit(_) => IpError::Timeout { until: None }.into(),
BybitErrorCode::ComplianceRules(_) => IpError::GeoBlocked { msg: e.msg }.into(),
_ => ApiError::Other(eyre!("Bybit error {}: {}", e.code.as_i32(), e.msg)),
}
}
}
impl From<i32> for BybitErrorCode {
fn from(code: i32) -> Self {
match code {
0 => Self::Ok,
10003 => Self::InvalidApiKey(code),
10004 => Self::ErrorSign(code),
10005 => Self::PermissionDenied(code),
10006 => Self::TooManyVisits(code),
10007 => Self::AuthenticationFailed(code),
10009 => Self::IpBanned(code),
10010 => Self::UnmatchedIp(code),
10014 => Self::InvalidDuplicateRequest(code),
10016 => Self::ServerError(code),
10017 => Self::RouteNotFound(code),
10018 => Self::IpRateLimit(code),
10024 => Self::ComplianceRules(code),
33004 => Self::ApiKeyExpired(code),
110001 => Self::OrderNotExist(code),
110007 => Self::InsufficientBalance(code),
code => {
tracing::warn!("Encountered unknown Bybit error code: {code}");
Self::Other(code)
}
}
}
}
impl From<BybitErrorCode> for i32 {
fn from(code: BybitErrorCode) -> Self {
code.as_i32()
}
}
impl<B, R> RequestHandler<B> for BybitRequestHandler<'_, R>
where
B: Serialize,
R: DeserializeOwned,
{
type Successful = R;
fn base_url(&self, is_test: bool) -> Result<Url, UrlError> {
match is_test {
true => self.options.http_url.url_testnet().ok_or_else(|| UrlError::MissingTestnet(self.options.http_url.url_mainnet())),
false => Ok(self.options.http_url.url_mainnet()),
}
}
fn build_request(&self, mut builder: RequestBuilder, request_body: &Option<B>, _: u8) -> Result<Request, BuildError> {
if self.options.http_auth == BybitHttpAuth::None {
if let Some(body) = request_body {
let json = serde_json::to_string(body)?;
builder = builder.header(header::CONTENT_TYPE, "application/json").body(json);
}
return Ok(builder.build().expect("My understanding is client can't trigger this. So fail fast for dev"));
}
let pubkey = self.options.pubkey.as_deref().ok_or(ConstructAuthError::new_missing_pubkey())?;
let secret = self.options.secret.as_ref().map(|s| s.expose_secret()).ok_or(ConstructAuthError::new_missing_secret())?;
let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); let timestamp = time.as_millis();
let hmac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
match self.options.http_auth {
BybitHttpAuth::SpotV1 => Self::v1_auth(builder, request_body, pubkey, timestamp, hmac, true, self.options.recv_window),
BybitHttpAuth::BelowV3 => Self::v1_auth(builder, request_body, pubkey, timestamp, hmac, false, self.options.recv_window),
BybitHttpAuth::UsdcContractV1 => Self::v3_auth(builder, request_body, pubkey, timestamp, hmac, true, self.options.recv_window),
BybitHttpAuth::V3AndAbove => Self::v3_auth(builder, request_body, pubkey, timestamp, hmac, false, self.options.recv_window),
BybitHttpAuth::None => unreachable!("we're already handled this case"),
}
}
fn rate_limit_key_name(&self) -> Option<String> {
use sha2::{Digest as _, Sha256};
self.options.pubkey.as_deref().map(|k| hex::encode(&Sha256::digest(k.as_bytes())[..4]))
}
fn handle_response(&self, status: StatusCode, _: HeaderMap, response_body: Bytes) -> Result<Self::Successful, HandleError> {
if status.is_success() {
let value: serde_json::Value = serde_json::from_slice(&response_body).map_err(|error| {
let response_str = v_utils::utils::truncate_msg(String::from_utf8_lossy(&response_body));
HandleError::Parse(eyre!("Failed to parse response: {error}\nResponse body: {response_str}"))
})?;
if let Some(ret_code) = value.get("retCode").and_then(|v| v.as_i64())
&& ret_code != 0
{
let ret_msg = value.get("retMsg").and_then(|v| v.as_str()).unwrap_or("Unknown error");
let error = BybitError {
code: BybitErrorCode::from(ret_code as i32),
msg: ret_msg.to_string(),
};
return Err(ApiError::from(error).into());
}
serde_json::from_value(value.clone()).map_err(|error| {
let response_str = v_utils::utils::truncate_msg(value.to_string());
HandleError::Parse(eyre!("Failed to parse successful response: {error}\nResponse body: {response_str}"))
})
} else {
if status == 403 {
let msg = std::str::from_utf8(&response_body).unwrap_or("<non-utf8 body>").to_string();
return Err(ApiError::from(IpError::Waf { msg }).into());
}
if status == 401 {
use v_exchanges_api_generics::http::AuthError;
let msg = match std::str::from_utf8(&response_body) {
Ok(s) if !s.is_empty() => s.to_string(),
_ => "HTTP 401 Unauthorized".to_string(),
};
return Err(ApiError::Auth(AuthError::Unauthorized { msg }).into());
}
let api_error: BybitError = match serde_json::from_slice(&response_body) {
Ok(parsed) => parsed,
Err(error) => {
let response_str = v_utils::utils::truncate_msg(String::from_utf8_lossy(&response_body));
return Err(HandleError::Parse(eyre!("Failed to parse error response: {error}\nResponse body: {response_str}")));
}
};
Err(ApiError::from(api_error).into())
}
}
}
impl WsHandler for BybitWsHandler {
fn config(&self) -> Result<WsConfig, UrlError> {
let mut config = self.options.ws_config.clone();
if self.options.ws_url != BybitWsUrlBase::None {
config.base_url = match self.options.testnet {
true => Some(self.options.ws_url.url_testnet().ok_or_else(|| UrlError::MissingTestnet(self.options.ws_url.url_mainnet()))?),
false => Some(self.options.ws_url.url_mainnet()),
}
}
config.topics = config.topics.union(&self.options.ws_topics).cloned().collect();
Ok(config)
}
#[instrument(skip_all)]
fn handle_auth(&mut self) -> Result<Vec<tungstenite::Message>, WsError> {
match self.options.ws_auth {
true => {
let pubkey = self.options.pubkey.as_ref().ok_or(ConstructAuthError::new_missing_pubkey())?;
let secret = self.options.secret.as_ref().ok_or(ConstructAuthError::new_missing_secret())?;
let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).expect("always after the epoch");
let expires = time.as_millis() as u64 + 1000;
let mut hmac = Hmac::<Sha256>::new_from_slice(secret.expose_secret().as_bytes()).expect("hmac accepts key of any length");
hmac.update(format!("GET/realtime{expires}").as_bytes());
let signature = hex::encode(hmac.finalize().into_bytes());
Ok(vec![tungstenite::Message::Text(
json!({
"op": "auth",
"args": [pubkey, expires, signature],
})
.to_string()
.into(),
)])
}
false => self.handle_subscribe(self.options.ws_topics.iter().cloned().map(Topic::String).collect()),
}
}
fn active_ping(&self) -> Vec<tungstenite::Message> {
vec![tungstenite::Message::Text(json!({ "op": "ping" }).to_string().into())]
}
fn handle_subscribe(&mut self, topics: AHashSet<Topic>) -> Result<Vec<tungstenite::Message>, WsError> {
let topics: Vec<String> = topics
.into_iter()
.map(|topic| match topic {
Topic::String(s) => s,
Topic::Order(_) => todo!(),
})
.collect();
Ok(vec![tungstenite::Message::Text(json!({ "op": "subscribe", "args": topics }).to_string().into())])
}
#[instrument(skip_all, fields(jrpc = ?format_args!("{:#?}", jrpc)))]
fn handle_jrpc(&mut self, jrpc: serde_json::Value) -> Result<ResponseOrContent, WsError> {
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(untagged)]
enum BybitResponse {
Feedback(FeedbackResponse),
Content(ContentResponse),
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
struct FeedbackResponse {
success: bool,
ret_msg: String,
op: Operation,
req_id: Option<String>,
conn_id: String,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "lowercase")]
enum Operation {
Auth,
Subscribe,
Unsubscribe,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct ContentResponse {
pub data: serde_json::Value,
pub topic: String,
pub ts: i64,
#[serde(rename = "type")]
pub event_type: String,
}
impl From<ContentResponse> for ContentEvent {
fn from(content: ContentResponse) -> Self {
ContentEvent {
topic: content.topic,
data: content.data,
time: Timestamp::from_millisecond(content.ts).unwrap(),
event_type: content.event_type,
}
}
}
let bybit_response = serde_json::from_value::<BybitResponse>(jrpc.clone()).wrap_err_with(|| format!("Failed to deserialize Bybit response: {jrpc:?}"))?;
match bybit_response {
BybitResponse::Feedback(FeedbackResponse { op, success, ret_msg, .. }) => match op {
Operation::Auth => match success {
true => {
tracing::info!("Ws authentication successful");
Ok(ResponseOrContent::Response(
self.handle_subscribe(self.options.ws_topics.clone().into_iter().map(Topic::String).collect())?,
))
}
false => Err(ConstructAuthError::Other(eyre!("Authentication was not successful: {ret_msg}")).into()),
},
Operation::Subscribe => {
if jrpc["success"].as_bool() == Some(true) {
tracing::info!("Ws topics subscription successful");
} else {
match self.options.ws_auth || &ret_msg != "Request not authorized" {
true => return Err(WsError::Subscription(ret_msg)),
false => {
return Err(ConstructAuthError::Other(eyre!("Tried to access a private endpoint without authentication")).into());
}
}
}
Ok(ResponseOrContent::Response(vec![]))
}
_ => todo!(),
},
BybitResponse::Content(content) => Ok(ResponseOrContent::Content(ContentEvent::from(content))),
}
}
}
impl EndpointUrl for BybitWsUrlBase {
fn url_mainnet(&self) -> Url {
match self {
Self::Bybit => Url::parse("wss://stream.bybit.com").unwrap(),
Self::Bytick => Url::parse("wss://stream.bytick.com").unwrap(),
Self::None => Url::parse("").unwrap(),
}
}
fn url_testnet(&self) -> Option<Url> {
match self {
Self::Bybit => Some(Url::parse("wss://stream-testnet.bybit.com").unwrap()),
Self::Bytick => None, Self::None => Some(Url::parse("").unwrap()),
}
}
}
impl WsOption for BybitOption {
type WsHandler = BybitWsHandler;
fn ws_handler(options: Self::Options) -> Self::WsHandler {
BybitWsHandler::new(options)
}
}
impl HandlerOptions for BybitOptions {
type OptionItem = BybitOption;
fn update(&mut self, option: Self::OptionItem) {
match option {
BybitOption::None => (),
BybitOption::Pubkey(v) => self.pubkey = Some(v),
BybitOption::Secret(v) => self.secret = Some(v),
BybitOption::Testnet(v) => self.testnet = v,
BybitOption::HttpUrl(v) => self.http_url = v,
BybitOption::HttpAuth(v) => self.http_auth = v,
BybitOption::RecvWindow(v) => self.recv_window = Some(v),
BybitOption::WsUrl(v) => self.ws_url = v,
BybitOption::WsAuth(v) => self.ws_auth = v,
BybitOption::WsConfig(v) => self.ws_config = v,
BybitOption::WsTopics(v) => self.ws_topics = v.into_iter().collect(),
}
}
fn is_authenticated(&self) -> bool {
self.pubkey.is_some() }
}
impl<'a, R, B> HttpOption<'a, R, B> for BybitOption
where
R: DeserializeOwned + 'a,
B: Serialize,
{
type RequestHandler = BybitRequestHandler<'a, R>;
fn request_handler(options: Self::Options) -> Self::RequestHandler {
BybitRequestHandler::<'a, R> { options, _phantom: PhantomData }
}
}
impl HandlerOption for BybitOption {
type Options = BybitOptions;
}