use std::{collections::HashSet, marker::PhantomData, str::FromStr, time::SystemTime};
use eyre::eyre;
use generics::{
ConstructAuthError, UrlError,
http::{ApiError, BuildError, HandleError, *},
tokio_tungstenite::tungstenite,
ws::{ContentEvent, ResponseOrContent, Topic, WsConfig, WsError, WsHandler},
};
use hmac::{Hmac, Mac};
use jiff::{SignedDuration, Timestamp};
use secrecy::{ExposeSecret as _, SecretString};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use sha2::Sha256;
use url::Url;
use v_utils::utils::truncate_msg;
use crate::traits::*;
impl<B, R> RequestHandler<B> for BinanceRequestHandler<'_, 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()),
}
}
#[tracing::instrument(skip_all, fields(?builder))]
fn build_request(&self, mut builder: RequestBuilder, request_body: &Option<B>, _: u8) -> Result<Request, BuildError> {
if let Some(body) = request_body {
let encoded = serde_urlencoded::to_string(body)?;
builder = builder.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded").body(encoded);
}
if self.options.http_auth != BinanceAuth::None {
let pubkey = self.options.pubkey.as_deref().ok_or(ConstructAuthError::MissingPubkey)?;
builder = builder.header("X-MBX-APIKEY", pubkey);
if self.options.http_auth == BinanceAuth::Sign {
let time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); let timestamp = time.as_millis();
builder = builder.query(&[("timestamp", timestamp)]);
if let Some(recv_window) = self.options.recv_window {
builder = builder.query(&[("recvWindow", recv_window.as_millis() as u64)]);
}
let secret = self.options.secret.as_ref().map(|s| s.expose_secret()).ok_or(ConstructAuthError::MissingSecret)?;
let mut hmac = Hmac::<Sha256>::new_from_slice(secret.as_bytes()).unwrap();
let mut request = builder.build().expect("From what I understand, can't trigger this from client-side");
let query = request.url().query().unwrap();
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);
return Ok(request);
}
}
Ok(builder.build().expect("don't expect this to be reached by client, so fail fast for dev"))
}
fn handle_response(&self, status: StatusCode, headers: HeaderMap, response_body: Bytes) -> Result<Self::Successful, HandleError> {
if status.is_success() {
serde_json::from_slice(&response_body).map_err(|error| {
let response_str = truncate_msg(String::from_utf8_lossy(&response_body));
HandleError::Parse(eyre!("Failed to parse 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 == 451 {
let msg = serde_json::from_slice::<serde_json::Value>(&response_body)
.ok()
.and_then(|v| v.get("msg").and_then(|m| m.as_str()).map(String::from))
.unwrap_or_else(|| String::from_utf8_lossy(&response_body).into_owned());
return Err(ApiError::from(IpError::GeoBlocked { msg }).into());
}
if status == 429 || status == 418 {
let retry_after_sec = if let Some(value) = headers.get("Retry-After") {
if let Ok(string) = value.to_str() {
if let Ok(retry_after) = u32::from_str(string) {
Some(retry_after)
} else {
tracing::debug!("Invalid number in Retry-After header");
None
}
} else {
tracing::debug!("Non-ASCII character in Retry-After header");
None
}
} else {
None
};
let e = match retry_after_sec {
Some(s) => {
let until = Some(Timestamp::now() + SignedDuration::from_secs(s as i64));
ApiError::from(IpError::Timeout { until }).into()
}
_ => eyre!("Could't interpret Retry-After header").into(),
};
return Err(e);
}
let e: BinanceError = match serde_json::from_slice::<BinanceError>(&response_body) {
Ok(binance_error) => binance_error,
Err(parse_error) => {
let response_str = truncate_msg(String::from_utf8_lossy(&response_body));
return Err(HandleError::Parse(eyre!("Failed to parse error response: {parse_error}\nResponse body: {response_str}")));
}
};
Err(ApiError::from(e).into())
}
}
}
#[derive(Clone, Debug)]
pub struct BinanceWsHandler {
options: BinanceOptions,
_last_keep_alive: SystemTime,
}
impl BinanceWsHandler {
pub fn new(options: BinanceOptions) -> Self {
Self {
options,
_last_keep_alive: SystemTime::UNIX_EPOCH, }
}
}
impl WsHandler for BinanceWsHandler {
fn config(&self) -> Result<WsConfig, UrlError> {
let mut config = self.options.ws_config.clone();
match self.options.ws_url {
BinanceWsUrl::None => tracing::warn!(
"BinanceWsUrl was not set. Due to Binance shenanigans, any provided topics will now be ignored, and must be manually hardcoded into the provided url on creation of the websocket. However, recommended approach is to simply provide a BinanceOption::WsUrl."
),
_ => {
let mut streams = String::new();
config.topics = config.topics.union(&self.options.ws_topics).cloned().collect();
for (i, topic) in config.topics.iter().enumerate() {
if i > 0 {
streams.push('/');
}
streams.push_str(topic);
}
let base_url = match self.options.test {
true => self.options.ws_url.url_testnet().ok_or_else(|| UrlError::MissingTestnet(self.options.ws_url.url_mainnet()))?,
false => self.options.ws_url.url_mainnet(),
};
config.base_url = Some(base_url.join(&format!("stream?streams={streams}")).unwrap());
}
}
Ok(config)
}
fn handle_auth(&mut self) -> Result<Vec<tungstenite::Message>, WsError> {
if self.options.ws_config.auth {
let _pubkey = self.options.pubkey.as_ref().ok_or(ConstructAuthError::MissingPubkey)?;
let _secret = self.options.secret.as_ref().ok_or(ConstructAuthError::MissingSecret)?;
}
Ok(vec![])
}
fn handle_subscribe(&mut self, topics: HashSet<Topic>) -> Result<Vec<tungstenite::Message>, WsError> {
let string_topics = topics
.iter()
.filter_map(|topic| if let Topic::String(s) = topic { Some(s) } else { None })
.cloned()
.collect::<Vec<_>>();
let messages = {
let msg = serde_json::json!({
"method": "SUBSCRIBE",
"params": string_topics,
"id": rand::random::<u64>(),
});
vec![tungstenite::Message::Text(msg.to_string().into())]
};
let order_topics = topics
.into_iter()
.filter_map(|topic| if let Topic::Order(v) = topic { Some(v) } else { None })
.collect::<Vec<_>>();
for _o in order_topics {
todo!();
}
Ok(messages)
}
fn handle_jrpc(&mut self, jrpc: serde_json::Value) -> Result<ResponseOrContent, WsError> {
#[derive(serde::Deserialize)]
struct NamedStreamData {
pub stream: String,
pub data: serde_json::Value,
}
let (event_topic, data) = {
match serde_json::from_value::<NamedStreamData>(jrpc.clone()) {
Ok(NamedStreamData { stream, data }) => (stream, data),
Err(_) => ("".to_string(), jrpc),
}
};
assert!(data.is_object(), "data should be an object");
let (event_type, event_time, event_data) = {
let mut event_data = data.as_object().unwrap().to_owned();
let event_type = data["e"].as_str().unwrap().to_owned();
event_data.remove("e");
let event_ts: i64 = data["E"].as_i64().unwrap();
let event_time = Timestamp::from_millisecond(event_ts).unwrap();
event_data.remove("E");
(event_type, event_time, event_data.into())
};
if event_type == "listenKeyExpired" {
tracing::error!("Listen key expired. This requires re-authentication and reconnection.");
return Err(WsError::Auth(ConstructAuthError::Other(eyre!("Listen key expired"))));
}
let content = ContentEvent {
data: event_data,
topic: event_topic,
time: event_time,
event_type,
};
Ok(ResponseOrContent::Content(content))
}
}
impl WsOption for BinanceOption {
type WsHandler = BinanceWsHandler;
fn ws_handler(options: Self::Options) -> Self::WsHandler {
BinanceWsHandler::new(options)
}
}
#[derive(Debug, Default)]
pub enum BinanceOption {
#[default]
None,
Pubkey(String),
Secret(SecretString),
Test(bool),
RecvWindow(std::time::Duration),
HttpUrl(BinanceHttpUrl),
HttpAuth(BinanceAuth),
WsUrl(BinanceWsUrl),
WsConfig(WsConfig),
WsTopics(Vec<String>),
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum BinanceHttpUrl {
Spot,
Spot1,
Spot2,
Spot3,
Spot4,
SpotData,
FuturesUsdM,
FuturesCoinM,
EuropeanOptions,
#[default]
None,
}
impl EndpointUrl for BinanceHttpUrl {
fn url_mainnet(&self) -> Url {
match self {
Self::Spot => Url::parse("https://api.binance.com").unwrap(),
Self::Spot1 => Url::parse("https://api1.binance.com").unwrap(),
Self::Spot2 => Url::parse("https://api2.binance.com").unwrap(),
Self::Spot3 => Url::parse("https://api3.binance.com").unwrap(),
Self::Spot4 => Url::parse("https://api4.binance.com").unwrap(),
Self::SpotData => Url::parse("https://data.binance.com").unwrap(),
Self::FuturesUsdM => Url::parse("https://fapi.binance.com").unwrap(),
Self::FuturesCoinM => Url::parse("https://dapi.binance.com").unwrap(),
Self::EuropeanOptions => Url::parse("https://eapi.binance.com").unwrap(),
Self::None => Url::parse("").unwrap(),
}
}
fn url_testnet(&self) -> Option<Url> {
match self {
Self::Spot => Some(Url::parse("https://testnet.binance.vision").unwrap()),
Self::Spot1 => Some(Url::parse("https://testnet.binance.vision").unwrap()),
Self::Spot2 => Some(Url::parse("https://testnet.binance.vision").unwrap()),
Self::Spot3 => Some(Url::parse("https://testnet.binance.vision").unwrap()),
Self::Spot4 => Some(Url::parse("https://testnet.binance.vision").unwrap()),
Self::SpotData => Some(Url::parse("https://testnet.binance.vision").unwrap()),
Self::FuturesUsdM => Some(Url::parse("https://testnet.binancefuture.com").unwrap()),
Self::FuturesCoinM => Some(Url::parse("https://testnet.binancefuture.com").unwrap()),
Self::EuropeanOptions => None,
Self::None => Some(Url::parse("").unwrap()),
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum BinanceWsUrl {
Spot,
Spot9443,
Spot443,
SpotData,
WebSocket443,
WebSocket9443,
FuturesUsdM,
FuturesUsdMAuth,
FuturesCoinM,
EuropeanOptions,
#[default]
None,
}
impl EndpointUrl for BinanceWsUrl {
fn url_mainnet(&self) -> url::Url {
match self {
Self::Spot => Url::parse("wss://stream.binance.com:9443").unwrap(), Self::Spot9443 => Url::parse("wss://stream.binance.com:9443").unwrap(),
Self::Spot443 => Url::parse("wss://stream.binance.com:443").unwrap(),
Self::SpotData => Url::parse("wss://data-stream.binance.com").unwrap(),
Self::WebSocket443 => Url::parse("wss://ws-api.binance.com:443").unwrap(),
Self::WebSocket9443 => Url::parse("wss://ws-api.binance.com:9443").unwrap(),
Self::FuturesUsdM => Url::parse("wss://fstream.binance.com").unwrap(),
Self::FuturesUsdMAuth => Url::parse("wss://fstream-auth.binance.com").unwrap(),
Self::FuturesCoinM => Url::parse("wss://dstream.binance.com").unwrap(),
Self::EuropeanOptions => Url::parse("wss://nbstream.binance.com").unwrap(),
Self::None => Url::parse("").unwrap(),
}
}
fn url_testnet(&self) -> Option<url::Url> {
match self {
Self::Spot => Some(Url::parse("wss://testnet.binance.vision").unwrap()),
Self::Spot9443 => Some(Url::parse("wss://testnet.binance.vision:9443").unwrap()),
Self::Spot443 => Some(Url::parse("wss://testnet.binance.vision:443").unwrap()),
Self::FuturesUsdM => Some(Url::parse("wss://stream.binancefuture.com").unwrap()),
Self::FuturesCoinM => Some(Url::parse("wss://dstream.binancefuture.com").unwrap()),
Self::SpotData | Self::WebSocket443 | Self::WebSocket9443 | Self::FuturesUsdMAuth | Self::EuropeanOptions | Self::None => None,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum BinanceAuth {
Sign,
Key, #[default]
None,
}
#[non_exhaustive]
#[derive(Debug)]
pub enum BinanceHandlerError {
ApiError(BinanceError),
RateLimitError { retry_after: Option<u32> },
ParseError,
}
pub struct BinanceRequestHandler<'a, R: DeserializeOwned> {
options: BinanceOptions,
_phantom: PhantomData<&'a R>,
}
#[derive(Clone, derive_more::Debug, Default)]
pub struct BinanceOptions {
pub pubkey: Option<String>,
#[debug("[REDACTED]")]
pub secret: Option<SecretString>,
pub recv_window: Option<std::time::Duration>,
pub http_url: BinanceHttpUrl,
pub http_auth: BinanceAuth,
pub ws_url: BinanceWsUrl,
pub ws_config: WsConfig,
pub ws_topics: HashSet<String>,
pub test: bool,
}
impl HandlerOptions for BinanceOptions {
type OptionItem = BinanceOption;
fn update(&mut self, option: Self::OptionItem) {
match option {
Self::OptionItem::None => (),
Self::OptionItem::Pubkey(v) => self.pubkey = Some(v),
Self::OptionItem::RecvWindow(v) => self.recv_window = Some(v),
Self::OptionItem::Test(v) => self.test = v,
Self::OptionItem::Secret(v) => self.secret = Some(v),
Self::OptionItem::HttpUrl(v) => self.http_url = v,
Self::OptionItem::HttpAuth(v) => self.http_auth = v,
Self::OptionItem::WsUrl(v) => self.ws_url = v,
Self::OptionItem::WsConfig(v) => self.ws_config = v,
Self::OptionItem::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 BinanceOption
where
R: DeserializeOwned + 'a,
B: Serialize,
{
type RequestHandler = BinanceRequestHandler<'a, R>;
fn request_handler(options: Self::Options) -> Self::RequestHandler {
BinanceRequestHandler::<'a, R> { options, _phantom: PhantomData }
}
}
impl HandlerOption for BinanceOption {
type Options = BinanceOptions;
}
#[derive(Clone, Debug, Deserialize)]
pub struct BinanceError {
pub code: BinanceErrorCode,
pub msg: String,
}
impl From<BinanceError> for ApiError {
fn from(e: BinanceError) -> Self {
use generics::http::AuthError;
match e.code {
BinanceErrorCode::RejectedMbxKey(_) | BinanceErrorCode::InvalidListenKey(_) => AuthError::KeyExpired { msg: e.msg }.into(),
BinanceErrorCode::Unauthorized(_) | BinanceErrorCode::InvalidSignature(_) | BinanceErrorCode::BadApiKeyFmt(_) => AuthError::Unauthorized { msg: e.msg }.into(),
_ => ApiError::Other(eyre!("Binance API error: {}", e.msg)),
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
#[serde(from = "i32")]
pub enum BinanceErrorCode {
Unknown(i32),
Disconnected(i32),
Unauthorized(i32),
TooManyRequests(i32),
UnexpectedResponse(i32),
Timeout(i32),
ServerBusy(i32),
InvalidMessage(i32),
UnknownOrderComposition(i32),
TooManyOrders(i32),
ServiceShuttingDown(i32),
UnsupportedOperation(i32),
InvalidTimestamp(i32),
InvalidSignature(i32),
IllegalChars(i32),
TooManyParameters(i32),
MandatoryParamEmptyOrMalformed(i32),
UnknownParam(i32),
UnreadParameters(i32),
ParamEmpty(i32),
ParamNotRequired(i32),
ParamOverflow(i32),
BadPrecision(i32),
NoDepth(i32),
TifNotRequired(i32),
InvalidTif(i32),
InvalidOrderType(i32),
InvalidSide(i32),
EmptyNewClOrdId(i32),
EmptyOrgClOrdId(i32),
BadInterval(i32),
BadSymbol(i32),
InvalidSymbolStatus(i32),
InvalidListenKey(i32),
MoreThanXXHours(i32),
OptionalParamsBadCombo(i32),
InvalidParameter(i32),
BadStrategyType(i32),
InvalidJson(i32),
InvalidTickerType(i32),
InvalidCancelRestrictions(i32),
DuplicateSymbols(i32),
InvalidSbeHeader(i32),
UnsupportedSchemaId(i32),
SbeDisabled(i32),
OcoOrderTypeRejected(i32),
OcoIcebergqtyTimeinforce(i32),
DeprecatedSchema(i32),
BuyOcoLimitMustBeBelow(i32),
SellOcoLimitMustBeAbove(i32),
BothOcoOrdersCannotBeLimit(i32),
InvalidTagNumber(i32),
TagNotDefinedInMessage(i32),
TagAppearsMoreThanOnce(i32),
TagOutOfOrder(i32),
GroupFieldsOutOfOrder(i32),
InvalidComponent(i32),
ResetSeqNumSupport(i32),
AlreadyLoggedIn(i32),
GarbledMessage(i32),
BadSenderCompid(i32),
BadSeqNum(i32),
ExpectedLogon(i32),
TooManyMessages(i32),
ParamsBadCombo(i32),
NotAllowedInDropCopySessions(i32),
DropCopySessionNotAllowed(i32),
DropCopySessionRequired(i32),
NotAllowedInOrderEntrySessions(i32),
NotAllowedInMarketDataSessions(i32),
IncorrectNumInGroupCount(i32),
DuplicateEntriesInAGroup(i32),
InvalidRequestId(i32),
TooManySubscriptions(i32),
BuyOcoStopLossMustBeAbove(i32),
SellOcoStopLossMustBeBelow(i32),
BuyOcoTakeProfitMustBeBelow(i32),
SellOcoTakeProfitMustBeAbove(i32),
NewOrderRejected(i32),
CancelRejected(i32),
NoSuchOrder(i32),
BadApiKeyFmt(i32),
RejectedMbxKey(i32),
NoTradingWindow(i32),
OrderArchived(i32),
OrderCancelReplacePartiallyFailed(i32),
OrderCancelReplaceFailed(i32),
Other(i32),
}
impl From<i32> for BinanceErrorCode {
fn from(code: i32) -> Self {
match code {
-1000 => Self::Unknown(code),
-1001 => Self::Disconnected(code),
-1002 => Self::Unauthorized(code),
-1003 => Self::TooManyRequests(code),
-1006 => Self::UnexpectedResponse(code),
-1007 => Self::Timeout(code),
-1008 => Self::ServerBusy(code),
-1013 => Self::InvalidMessage(code),
-1014 => Self::UnknownOrderComposition(code),
-1015 => Self::TooManyOrders(code),
-1016 => Self::ServiceShuttingDown(code),
-1020 => Self::UnsupportedOperation(code),
-1021 => Self::InvalidTimestamp(code),
-1022 => Self::InvalidSignature(code),
-1100 => Self::IllegalChars(code),
-1101 => Self::TooManyParameters(code),
-1102 => Self::MandatoryParamEmptyOrMalformed(code),
-1103 => Self::UnknownParam(code),
-1104 => Self::UnreadParameters(code),
-1105 => Self::ParamEmpty(code),
-1106 => Self::ParamNotRequired(code),
-1108 => Self::ParamOverflow(code),
-1111 => Self::BadPrecision(code),
-1112 => Self::NoDepth(code),
-1114 => Self::TifNotRequired(code),
-1115 => Self::InvalidTif(code),
-1116 => Self::InvalidOrderType(code),
-1117 => Self::InvalidSide(code),
-1118 => Self::EmptyNewClOrdId(code),
-1119 => Self::EmptyOrgClOrdId(code),
-1120 => Self::BadInterval(code),
-1121 => Self::BadSymbol(code),
-1122 => Self::InvalidSymbolStatus(code),
-1125 => Self::InvalidListenKey(code),
-1127 => Self::MoreThanXXHours(code),
-1128 => Self::OptionalParamsBadCombo(code),
-1130 => Self::InvalidParameter(code),
-1134 => Self::BadStrategyType(code),
-1135 => Self::InvalidJson(code),
-1139 => Self::InvalidTickerType(code),
-1145 => Self::InvalidCancelRestrictions(code),
-1151 => Self::DuplicateSymbols(code),
-1152 => Self::InvalidSbeHeader(code),
-1153 => Self::UnsupportedSchemaId(code),
-1155 => Self::SbeDisabled(code),
-1158 => Self::OcoOrderTypeRejected(code),
-1160 => Self::OcoIcebergqtyTimeinforce(code),
-1161 => Self::DeprecatedSchema(code),
-1165 => Self::BuyOcoLimitMustBeBelow(code),
-1166 => Self::SellOcoLimitMustBeAbove(code),
-1168 => Self::BothOcoOrdersCannotBeLimit(code),
-1169 => Self::InvalidTagNumber(code),
-1170 => Self::TagNotDefinedInMessage(code),
-1171 => Self::TagAppearsMoreThanOnce(code),
-1172 => Self::TagOutOfOrder(code),
-1173 => Self::GroupFieldsOutOfOrder(code),
-1174 => Self::InvalidComponent(code),
-1175 => Self::ResetSeqNumSupport(code),
-1176 => Self::AlreadyLoggedIn(code),
-1177 => Self::GarbledMessage(code),
-1178 => Self::BadSenderCompid(code),
-1179 => Self::BadSeqNum(code),
-1180 => Self::ExpectedLogon(code),
-1181 => Self::TooManyMessages(code),
-1182 => Self::ParamsBadCombo(code),
-1183 => Self::NotAllowedInDropCopySessions(code),
-1184 => Self::DropCopySessionNotAllowed(code),
-1185 => Self::DropCopySessionRequired(code),
-1186 => Self::NotAllowedInOrderEntrySessions(code),
-1187 => Self::NotAllowedInMarketDataSessions(code),
-1188 => Self::IncorrectNumInGroupCount(code),
-1189 => Self::DuplicateEntriesInAGroup(code),
-1190 => Self::InvalidRequestId(code),
-1191 => Self::TooManySubscriptions(code),
-1196 => Self::BuyOcoStopLossMustBeAbove(code),
-1197 => Self::SellOcoStopLossMustBeBelow(code),
-1198 => Self::BuyOcoTakeProfitMustBeBelow(code),
-1199 => Self::SellOcoTakeProfitMustBeAbove(code),
-2010 => Self::NewOrderRejected(code),
-2011 => Self::CancelRejected(code),
-2013 => Self::NoSuchOrder(code),
-2014 => Self::BadApiKeyFmt(code),
-2015 => Self::RejectedMbxKey(code),
-2016 => Self::NoTradingWindow(code),
-2021 => Self::OrderCancelReplacePartiallyFailed(code),
-2022 => Self::OrderCancelReplaceFailed(code),
-2026 => Self::OrderArchived(code),
code => {
tracing::warn!("Encountered unknown Binance error code: {code}");
Self::Other(code)
}
}
}
}