1use std::sync::Arc;
12
13#[cfg(feature = "fix")]
14use deribit_fix::DeribitFixClient;
15#[cfg(feature = "fix")]
16use deribit_fix::config::DeribitFixConfig;
17use deribit_http::config::credentials::ApiCredentials;
18use deribit_http::{DeribitHttpClient, HttpConfig};
19use deribit_websocket::client::DeribitWebSocketClient;
20use deribit_websocket::config::WebSocketConfig;
21#[cfg(feature = "fix")]
22use tokio::sync::Mutex;
23use tokio::sync::OnceCell;
24use url::Url;
25
26use crate::config::Config;
27#[cfg(feature = "fix")]
28use crate::config::OrderTransport;
29use crate::error::AdapterError;
30
31const TESTNET_WS_URL: &str = "wss://test.deribit.com/ws/api/v2";
32const MAINNET_WS_URL: &str = "wss://www.deribit.com/ws/api/v2";
33
34pub struct AdapterContext {
46 pub config: Arc<Config>,
48 pub http: DeribitHttpClient,
51 ws: OnceCell<DeribitWebSocketClient>,
54 #[cfg(feature = "fix")]
60 fix: OnceCell<Arc<Mutex<DeribitFixClient>>>,
61}
62
63impl std::fmt::Debug for AdapterContext {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 let mut s = f.debug_struct("AdapterContext");
66 s.field("config", &self.config)
67 .field("http", &"<DeribitHttpClient>")
68 .field("ws", &self.ws);
69 #[cfg(feature = "fix")]
70 s.field(
71 "fix",
72 &if self.fix.initialized() {
73 "<fix client>"
74 } else {
75 "<not initialized>"
76 },
77 );
78 s.finish()
79 }
80}
81
82impl AdapterContext {
83 pub fn new(config: Arc<Config>) -> Result<Self, AdapterError> {
91 let http_cfg = http_config_from(&config)?;
92 let http = DeribitHttpClient::with_config(http_cfg);
93
94 Ok(Self {
95 config,
96 http,
97 ws: OnceCell::new(),
98 #[cfg(feature = "fix")]
99 fix: OnceCell::new(),
100 })
101 }
102
103 #[must_use]
107 pub fn has_credentials(&self) -> bool {
108 self.config.client_id.is_some() && self.config.client_secret.is_some()
109 }
110
111 #[must_use]
122 pub fn auth_state(&self) -> AuthState {
123 if self.has_credentials() {
124 AuthState::Configured
125 } else {
126 AuthState::Anonymous
127 }
128 }
129
130 pub async fn websocket(&self) -> Result<&DeribitWebSocketClient, AdapterError> {
141 self.ws
142 .get_or_try_init(|| async {
143 let cfg = ws_config_from(&self.config);
144 DeribitWebSocketClient::new(&cfg)
145 })
146 .await
147 .map_err(AdapterError::from)
148 }
149
150 #[cfg(feature = "fix")]
173 pub async fn ensure_fix(&self) -> Result<Arc<Mutex<DeribitFixClient>>, AdapterError> {
174 match self.config.order_transport {
175 OrderTransport::Fix => {}
176 OrderTransport::Http => {
177 return Err(AdapterError::validation(
178 "order_transport",
179 "ensure_fix called but configured order_transport is `http`",
180 ));
181 }
182 }
183 let handle = self
184 .fix
185 .get_or_try_init(|| async {
186 let cfg = fix_config_from(&self.config)?;
187 let mut client = DeribitFixClient::new(&cfg).await?;
188 client.connect().await?;
189 Ok::<_, AdapterError>(Arc::new(Mutex::new(client)))
190 })
191 .await?;
192 Ok(handle.clone())
193 }
194
195 #[cfg(feature = "fix")]
205 pub async fn shutdown_fix(&self) -> Result<(), AdapterError> {
206 if let Some(handle) = self.fix.get() {
207 let mut guard = handle.lock().await;
208 guard.disconnect().await?;
209 }
210 Ok(())
211 }
212}
213
214fn http_config_from(config: &Config) -> Result<HttpConfig, AdapterError> {
223 let parsed = Url::parse(&config.endpoint)
224 .map_err(|err| AdapterError::validation("endpoint", format!("invalid URL: {err}")))?;
225
226 let testnet = !is_mainnet(&parsed);
227 let mut cfg = if testnet {
228 HttpConfig::testnet()
229 } else {
230 HttpConfig::production()
231 };
232 let user_supplied_path = !matches!(parsed.path(), "" | "/");
241 if user_supplied_path {
242 cfg.base_url = parsed;
243 }
244 cfg.testnet = testnet;
245 cfg.credentials = match (config.client_id.as_ref(), config.client_secret.as_ref()) {
250 (Some(client_id), Some(client_secret)) => Some(ApiCredentials {
251 client_id: Some(client_id.clone()),
252 client_secret: Some(client_secret.clone()),
253 }),
254 _ => None,
255 };
256 Ok(cfg)
257}
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub enum AuthState {
264 Anonymous,
266 Configured,
269}
270
271fn ws_config_from(config: &Config) -> WebSocketConfig {
277 let url = if endpoint_is_mainnet(&config.endpoint) {
278 MAINNET_WS_URL
279 } else {
280 TESTNET_WS_URL
281 };
282 WebSocketConfig::with_url(url).expect("compile-time WS URL constant must parse")
283}
284
285fn endpoint_is_mainnet(endpoint: &str) -> bool {
286 Url::parse(endpoint).ok().is_some_and(|u| is_mainnet(&u))
287}
288
289#[cfg(feature = "fix")]
298fn fix_config_from(config: &Config) -> Result<DeribitFixConfig, AdapterError> {
299 let (Some(client_id), Some(client_secret)) =
300 (config.client_id.as_ref(), config.client_secret.as_ref())
301 else {
302 return Err(AdapterError::validation(
303 "credentials",
304 "FIX transport requires DERIBIT_CLIENT_ID + DERIBIT_CLIENT_SECRET",
305 ));
306 };
307 let mainnet = endpoint_is_mainnet(&config.endpoint);
308 let (host, port) = if mainnet {
309 ("fix.deribit.com", 9881_u16)
310 } else {
311 ("fix-test.deribit.com", 9881_u16)
312 };
313 let mut fix_cfg =
314 DeribitFixConfig::new().with_credentials(client_id.clone(), client_secret.clone());
315 fix_cfg.host = host.to_string();
316 fix_cfg.port = port;
317 fix_cfg.use_ssl = false;
318 Ok(fix_cfg)
319}
320
321fn is_mainnet(url: &Url) -> bool {
322 matches!(url.host_str(), Some(host) if host == "www.deribit.com" || host == "deribit.com")
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use crate::config::{LogFormat, OrderTransport, Transport};
329 use std::net::SocketAddr;
330
331 fn cfg(endpoint: &str, with_creds: bool) -> Config {
332 Config {
333 endpoint: endpoint.to_string(),
334 client_id: with_creds.then(|| "id".to_string()),
335 client_secret: with_creds.then(|| "secret".to_string()),
336 allow_trading: false,
337 max_order_usd: None,
338 transport: Transport::Stdio,
339 http_listen: SocketAddr::from(([127, 0, 0, 1], 8723)),
340 http_bearer_token: None,
341 log_format: LogFormat::Text,
342 order_transport: OrderTransport::Http,
343 }
344 }
345
346 #[cfg(feature = "fix")]
347 #[tokio::test]
348 async fn ensure_fix_when_transport_is_http_returns_validation() {
349 let ctx =
353 AdapterContext::new(Arc::new(cfg("https://test.deribit.com", true))).expect("ctx");
354 match ctx.ensure_fix().await {
358 Ok(_) => panic!("expected Validation error, got Ok"),
359 Err(AdapterError::Validation { field, .. }) => {
360 assert_eq!(field, "order_transport");
361 }
362 Err(other) => panic!("unexpected: {other:?}"),
363 }
364 }
365
366 #[cfg(feature = "fix")]
367 #[tokio::test]
368 async fn ensure_fix_without_credentials_returns_validation() {
369 let mut config = cfg("https://test.deribit.com", false);
373 config.order_transport = OrderTransport::Fix;
374 config.allow_trading = true;
375 let ctx = AdapterContext::new(Arc::new(config)).expect("ctx");
376 match ctx.ensure_fix().await {
377 Ok(_) => panic!("expected Validation error, got Ok"),
378 Err(AdapterError::Validation { field, .. }) => {
379 assert_eq!(field, "credentials");
380 }
381 Err(other) => panic!("unexpected: {other:?}"),
382 }
383 }
384
385 #[cfg(feature = "fix")]
386 #[tokio::test]
387 async fn shutdown_fix_when_never_opened_is_noop() {
388 let ctx =
389 AdapterContext::new(Arc::new(cfg("https://test.deribit.com", true))).expect("ctx");
390 ctx.shutdown_fix().await.expect("noop ok");
391 }
392
393 #[test]
394 fn context_builds_for_testnet_endpoint() {
395 let ctx =
396 AdapterContext::new(Arc::new(cfg("https://test.deribit.com", false))).expect("context");
397 assert!(!ctx.has_credentials());
398 }
399
400 #[test]
401 fn context_builds_for_mainnet_endpoint() {
402 let ctx =
403 AdapterContext::new(Arc::new(cfg("https://www.deribit.com", true))).expect("context");
404 assert!(ctx.has_credentials());
405 }
406
407 #[test]
408 fn context_rejects_invalid_endpoint() {
409 let err = AdapterContext::new(Arc::new(cfg("not a url", false))).unwrap_err();
410 assert!(matches!(
411 err,
412 AdapterError::Validation { ref field, .. } if field == "endpoint"
413 ));
414 }
415
416 #[test]
417 fn has_credentials_requires_both_id_and_secret() {
418 let mut c = cfg("https://test.deribit.com", false);
419 c.client_id = Some("id".into());
420 let ctx = AdapterContext::new(Arc::new(c)).expect("context");
421 assert!(!ctx.has_credentials());
422 }
423
424 #[test]
425 fn auth_state_is_anonymous_without_credentials() {
426 let ctx =
427 AdapterContext::new(Arc::new(cfg("https://test.deribit.com", false))).expect("ctx");
428 assert_eq!(ctx.auth_state(), AuthState::Anonymous);
429 }
430
431 #[test]
432 fn auth_state_is_configured_with_credentials() {
433 let ctx =
434 AdapterContext::new(Arc::new(cfg("https://test.deribit.com", true))).expect("ctx");
435 assert_eq!(ctx.auth_state(), AuthState::Configured);
436 }
437
438 #[test]
439 fn http_config_carries_credentials_into_upstream() {
440 let resolved = cfg("https://test.deribit.com", true);
446 let http_cfg = http_config_from(&resolved).expect("http cfg");
447 let creds = http_cfg.credentials.as_ref().expect("credentials present");
448 assert_eq!(creds.client_id.as_deref(), Some("id"));
449 assert_eq!(creds.client_secret.as_deref(), Some("secret"));
450 }
451
452 #[test]
453 fn http_config_omits_credentials_without_both() {
454 let mut resolved = cfg("https://test.deribit.com", false);
455 resolved.client_id = Some("id".into());
456 let http_cfg = http_config_from(&resolved).expect("http cfg");
457 assert!(http_cfg.credentials.is_none());
458 }
459}