#![allow(unused_assignments)]
use std::fmt::Debug;
use aws_lc_rs::hmac;
use nautilus_core::{env::resolve_env_var_pair, hex, string::REDACTED};
use zeroize::ZeroizeOnDrop;
use crate::common::enums::BybitEnvironment;
#[must_use]
pub fn credential_env_vars(environment: BybitEnvironment) -> (&'static str, &'static str) {
match environment {
BybitEnvironment::Demo => ("BYBIT_DEMO_API_KEY", "BYBIT_DEMO_API_SECRET"),
BybitEnvironment::Testnet => ("BYBIT_TESTNET_API_KEY", "BYBIT_TESTNET_API_SECRET"),
BybitEnvironment::Mainnet => ("BYBIT_API_KEY", "BYBIT_API_SECRET"),
}
}
#[derive(Clone, ZeroizeOnDrop)]
pub struct Credential {
api_key: Box<str>,
api_secret: Box<[u8]>,
}
impl Debug for Credential {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!(Credential))
.field("api_key", &self.api_key)
.field("api_secret", &REDACTED)
.finish()
}
}
impl Credential {
#[must_use]
pub fn resolve(
api_key: Option<String>,
api_secret: Option<String>,
environment: BybitEnvironment,
) -> Option<Self> {
let (key_var, secret_var) = credential_env_vars(environment);
let (k, s) = resolve_env_var_pair(api_key, api_secret, key_var, secret_var)?;
Some(Self::new(k, s))
}
#[must_use]
pub fn new(api_key: impl Into<String>, api_secret: impl Into<String>) -> Self {
Self {
api_key: api_key.into().into_boxed_str(),
api_secret: api_secret.into().into_bytes().into_boxed_slice(),
}
}
#[must_use]
pub fn api_key(&self) -> &str {
&self.api_key
}
#[must_use]
pub fn api_key_masked(&self) -> String {
nautilus_core::string::mask_api_key(&self.api_key)
}
#[must_use]
pub fn sign_websocket_auth(&self, expires: i64) -> String {
let message = format!("GET/realtime{expires}");
let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
let tag = hmac::sign(&key, message.as_bytes());
hex::encode(tag.as_ref())
}
#[must_use]
pub fn sign_with_payload(
&self,
timestamp: &str,
recv_window_ms: u64,
payload: Option<&str>,
) -> String {
let recv_window = recv_window_ms.to_string();
let payload_len = payload.map_or(0usize, str::len);
let mut message = String::with_capacity(
timestamp.len() + self.api_key.len() + recv_window.len() + payload_len,
);
message.push_str(timestamp);
message.push_str(&self.api_key);
message.push_str(&recv_window);
if let Some(payload) = payload {
message.push_str(payload);
}
let key = hmac::Key::new(hmac::HMAC_SHA256, &self.api_secret);
let tag = hmac::sign(&key, message.as_bytes());
hex::encode(tag.as_ref())
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
const API_KEY: &str = "test_api_key";
const API_SECRET: &str = "test_secret";
const RECV_WINDOW: u64 = 5_000;
const TIMESTAMP: &str = "1700000000000";
#[rstest]
fn sign_with_payload_matches_reference_get() {
let credential = Credential::new(API_KEY, API_SECRET);
let query = "category=linear&symbol=BTCUSDT";
let signature = credential.sign_with_payload(TIMESTAMP, RECV_WINDOW, Some(query));
assert_eq!(
signature,
"fd4f31228a46109dc6673062328693696df9a96c7ff04e6491a45e7f63a0fdd7"
);
}
#[rstest]
fn sign_with_payload_matches_reference_post() {
let credential = Credential::new(API_KEY, API_SECRET);
let body = "{\"category\": \"linear\", \"symbol\": \"BTCUSDT\", \"orderLinkId\": \"test-order-1\"}";
let signature = credential.sign_with_payload(TIMESTAMP, RECV_WINDOW, Some(body));
assert_eq!(
signature,
"2df4a0603d69c08d5dea29ba85b46eb7db64ce9e9ebd34a7802a3d69700cb2a1"
);
}
#[rstest]
fn sign_with_empty_payload_omits_tail() {
let credential = Credential::new(API_KEY, API_SECRET);
let signature = credential.sign_with_payload(TIMESTAMP, RECV_WINDOW, None);
let expected = credential.sign_with_payload(TIMESTAMP, RECV_WINDOW, Some(""));
assert_eq!(signature, expected);
}
#[rstest]
fn sign_websocket_auth_matches_reference() {
let credential = Credential::new(API_KEY, API_SECRET);
let expires: i64 = 1_700_000_000_000;
let signature = credential.sign_websocket_auth(expires);
assert_eq!(
signature,
"bacffe7500499eb829bb58c45d36d1b3e5ac67c14eaeba91df5e99ccee013925"
);
}
#[rstest]
fn test_debug_redacts_secret() {
let credential = Credential::new(API_KEY, API_SECRET);
let dbg_out = format!("{credential:?}");
assert!(dbg_out.contains(REDACTED));
assert!(!dbg_out.contains(API_SECRET));
}
#[rstest]
fn test_resolve_with_both_args() {
let result = Credential::resolve(
Some("my_key".to_string()),
Some("my_secret".to_string()),
BybitEnvironment::Mainnet,
);
assert!(result.is_some());
assert_eq!(result.unwrap().api_key(), "my_key");
}
#[rstest]
fn test_resolve_with_no_args_no_env() {
let (key_var, secret_var) = credential_env_vars(BybitEnvironment::Mainnet);
if std::env::var(key_var).is_ok() || std::env::var(secret_var).is_ok() {
return;
}
let result = Credential::resolve(None, None, BybitEnvironment::Mainnet);
assert!(result.is_none());
}
}