use std::sync::Arc;
#[cfg(feature = "fix")]
use deribit_fix::DeribitFixClient;
#[cfg(feature = "fix")]
use deribit_fix::config::DeribitFixConfig;
use deribit_http::config::credentials::ApiCredentials;
use deribit_http::{DeribitHttpClient, HttpConfig};
use deribit_websocket::client::DeribitWebSocketClient;
use deribit_websocket::config::WebSocketConfig;
#[cfg(feature = "fix")]
use tokio::sync::Mutex;
use tokio::sync::OnceCell;
use url::Url;
use crate::config::Config;
#[cfg(feature = "fix")]
use crate::config::OrderTransport;
use crate::error::AdapterError;
const TESTNET_WS_URL: &str = "wss://test.deribit.com/ws/api/v2";
const MAINNET_WS_URL: &str = "wss://www.deribit.com/ws/api/v2";
pub struct AdapterContext {
pub config: Arc<Config>,
pub http: DeribitHttpClient,
ws: OnceCell<DeribitWebSocketClient>,
#[cfg(feature = "fix")]
fix: OnceCell<Arc<Mutex<DeribitFixClient>>>,
}
impl std::fmt::Debug for AdapterContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("AdapterContext");
s.field("config", &self.config)
.field("http", &"<DeribitHttpClient>")
.field("ws", &self.ws);
#[cfg(feature = "fix")]
s.field(
"fix",
&if self.fix.initialized() {
"<fix client>"
} else {
"<not initialized>"
},
);
s.finish()
}
}
impl AdapterContext {
pub fn new(config: Arc<Config>) -> Result<Self, AdapterError> {
let http_cfg = http_config_from(&config)?;
let http = DeribitHttpClient::with_config(http_cfg);
Ok(Self {
config,
http,
ws: OnceCell::new(),
#[cfg(feature = "fix")]
fix: OnceCell::new(),
})
}
#[must_use]
pub fn has_credentials(&self) -> bool {
self.config.client_id.is_some() && self.config.client_secret.is_some()
}
#[must_use]
pub fn auth_state(&self) -> AuthState {
if self.has_credentials() {
AuthState::Configured
} else {
AuthState::Anonymous
}
}
pub async fn websocket(&self) -> Result<&DeribitWebSocketClient, AdapterError> {
self.ws
.get_or_try_init(|| async {
let cfg = ws_config_from(&self.config);
DeribitWebSocketClient::new(&cfg)
})
.await
.map_err(AdapterError::from)
}
#[cfg(feature = "fix")]
pub async fn ensure_fix(&self) -> Result<Arc<Mutex<DeribitFixClient>>, AdapterError> {
match self.config.order_transport {
OrderTransport::Fix => {}
OrderTransport::Http => {
return Err(AdapterError::validation(
"order_transport",
"ensure_fix called but configured order_transport is `http`",
));
}
}
let handle = self
.fix
.get_or_try_init(|| async {
let cfg = fix_config_from(&self.config)?;
let mut client = DeribitFixClient::new(&cfg).await?;
client.connect().await?;
Ok::<_, AdapterError>(Arc::new(Mutex::new(client)))
})
.await?;
Ok(handle.clone())
}
#[cfg(feature = "fix")]
pub async fn shutdown_fix(&self) -> Result<(), AdapterError> {
if let Some(handle) = self.fix.get() {
let mut guard = handle.lock().await;
guard.disconnect().await?;
}
Ok(())
}
}
fn http_config_from(config: &Config) -> Result<HttpConfig, AdapterError> {
let parsed = Url::parse(&config.endpoint)
.map_err(|err| AdapterError::validation("endpoint", format!("invalid URL: {err}")))?;
let testnet = !is_mainnet(&parsed);
let mut cfg = if testnet {
HttpConfig::testnet()
} else {
HttpConfig::production()
};
let user_supplied_path = !matches!(parsed.path(), "" | "/");
if user_supplied_path {
cfg.base_url = parsed;
}
cfg.testnet = testnet;
cfg.credentials = match (config.client_id.as_ref(), config.client_secret.as_ref()) {
(Some(client_id), Some(client_secret)) => Some(ApiCredentials {
client_id: Some(client_id.clone()),
client_secret: Some(client_secret.clone()),
}),
_ => None,
};
Ok(cfg)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthState {
Anonymous,
Configured,
}
fn ws_config_from(config: &Config) -> WebSocketConfig {
let url = if endpoint_is_mainnet(&config.endpoint) {
MAINNET_WS_URL
} else {
TESTNET_WS_URL
};
WebSocketConfig::with_url(url).expect("compile-time WS URL constant must parse")
}
fn endpoint_is_mainnet(endpoint: &str) -> bool {
Url::parse(endpoint).ok().is_some_and(|u| is_mainnet(&u))
}
#[cfg(feature = "fix")]
fn fix_config_from(config: &Config) -> Result<DeribitFixConfig, AdapterError> {
let (Some(client_id), Some(client_secret)) =
(config.client_id.as_ref(), config.client_secret.as_ref())
else {
return Err(AdapterError::validation(
"credentials",
"FIX transport requires DERIBIT_CLIENT_ID + DERIBIT_CLIENT_SECRET",
));
};
let mainnet = endpoint_is_mainnet(&config.endpoint);
let (host, port) = if mainnet {
("fix.deribit.com", 9881_u16)
} else {
("fix-test.deribit.com", 9881_u16)
};
let mut fix_cfg =
DeribitFixConfig::new().with_credentials(client_id.clone(), client_secret.clone());
fix_cfg.host = host.to_string();
fix_cfg.port = port;
fix_cfg.use_ssl = false;
Ok(fix_cfg)
}
fn is_mainnet(url: &Url) -> bool {
matches!(url.host_str(), Some(host) if host == "www.deribit.com" || host == "deribit.com")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{LogFormat, OrderTransport, Transport};
use std::net::SocketAddr;
fn cfg(endpoint: &str, with_creds: bool) -> Config {
Config {
endpoint: endpoint.to_string(),
client_id: with_creds.then(|| "id".to_string()),
client_secret: with_creds.then(|| "secret".to_string()),
allow_trading: false,
max_order_usd: None,
transport: Transport::Stdio,
http_listen: SocketAddr::from(([127, 0, 0, 1], 8723)),
http_bearer_token: None,
log_format: LogFormat::Text,
order_transport: OrderTransport::Http,
}
}
#[cfg(feature = "fix")]
#[tokio::test]
async fn ensure_fix_when_transport_is_http_returns_validation() {
let ctx =
AdapterContext::new(Arc::new(cfg("https://test.deribit.com", true))).expect("ctx");
match ctx.ensure_fix().await {
Ok(_) => panic!("expected Validation error, got Ok"),
Err(AdapterError::Validation { field, .. }) => {
assert_eq!(field, "order_transport");
}
Err(other) => panic!("unexpected: {other:?}"),
}
}
#[cfg(feature = "fix")]
#[tokio::test]
async fn ensure_fix_without_credentials_returns_validation() {
let mut config = cfg("https://test.deribit.com", false);
config.order_transport = OrderTransport::Fix;
config.allow_trading = true;
let ctx = AdapterContext::new(Arc::new(config)).expect("ctx");
match ctx.ensure_fix().await {
Ok(_) => panic!("expected Validation error, got Ok"),
Err(AdapterError::Validation { field, .. }) => {
assert_eq!(field, "credentials");
}
Err(other) => panic!("unexpected: {other:?}"),
}
}
#[cfg(feature = "fix")]
#[tokio::test]
async fn shutdown_fix_when_never_opened_is_noop() {
let ctx =
AdapterContext::new(Arc::new(cfg("https://test.deribit.com", true))).expect("ctx");
ctx.shutdown_fix().await.expect("noop ok");
}
#[test]
fn context_builds_for_testnet_endpoint() {
let ctx =
AdapterContext::new(Arc::new(cfg("https://test.deribit.com", false))).expect("context");
assert!(!ctx.has_credentials());
}
#[test]
fn context_builds_for_mainnet_endpoint() {
let ctx =
AdapterContext::new(Arc::new(cfg("https://www.deribit.com", true))).expect("context");
assert!(ctx.has_credentials());
}
#[test]
fn context_rejects_invalid_endpoint() {
let err = AdapterContext::new(Arc::new(cfg("not a url", false))).unwrap_err();
assert!(matches!(
err,
AdapterError::Validation { ref field, .. } if field == "endpoint"
));
}
#[test]
fn has_credentials_requires_both_id_and_secret() {
let mut c = cfg("https://test.deribit.com", false);
c.client_id = Some("id".into());
let ctx = AdapterContext::new(Arc::new(c)).expect("context");
assert!(!ctx.has_credentials());
}
#[test]
fn auth_state_is_anonymous_without_credentials() {
let ctx =
AdapterContext::new(Arc::new(cfg("https://test.deribit.com", false))).expect("ctx");
assert_eq!(ctx.auth_state(), AuthState::Anonymous);
}
#[test]
fn auth_state_is_configured_with_credentials() {
let ctx =
AdapterContext::new(Arc::new(cfg("https://test.deribit.com", true))).expect("ctx");
assert_eq!(ctx.auth_state(), AuthState::Configured);
}
#[test]
fn http_config_carries_credentials_into_upstream() {
let resolved = cfg("https://test.deribit.com", true);
let http_cfg = http_config_from(&resolved).expect("http cfg");
let creds = http_cfg.credentials.as_ref().expect("credentials present");
assert_eq!(creds.client_id.as_deref(), Some("id"));
assert_eq!(creds.client_secret.as_deref(), Some("secret"));
}
#[test]
fn http_config_omits_credentials_without_both() {
let mut resolved = cfg("https://test.deribit.com", false);
resolved.client_id = Some("id".into());
let http_cfg = http_config_from(&resolved).expect("http cfg");
assert!(http_cfg.credentials.is_none());
}
}