nautilus_deribit/websocket/
auth.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Authentication state and token refresh for Deribit WebSocket connections.
17
18use std::time::Duration;
19
20use nautilus_common::live::get_runtime;
21use nautilus_core::{UUID4, time::get_atomic_clock_realtime};
22
23use super::{
24    handler::HandlerCommand,
25    messages::{DeribitAuthParams, DeribitAuthResult, DeribitRefreshTokenParams},
26};
27use crate::common::credential::Credential;
28
29/// Session name for Deribit WebSocket data client authentication.
30pub const DERIBIT_DATA_SESSION_NAME: &str = "nautilus-data";
31
32/// Session name for Deribit WebSocket execution client authentication.
33pub const DERIBIT_EXECUTION_SESSION_NAME: &str = "nautilus-execution";
34
35/// Authentication state storing OAuth tokens.
36#[derive(Debug, Clone)]
37pub struct AuthState {
38    /// Access token for API requests.
39    pub access_token: String,
40    /// Refresh token for obtaining new access tokens.
41    pub refresh_token: String,
42    /// Token expiration time in seconds from authentication.
43    pub expires_in: u64,
44    /// Timestamp when tokens were obtained (Unix milliseconds).
45    pub obtained_at: u64,
46    /// Scope used for authentication.
47    pub scope: String,
48}
49
50impl AuthState {
51    /// Creates a new [`AuthState`] from an authentication result.
52    #[must_use]
53    pub fn from_auth_result(result: &DeribitAuthResult, obtained_at: u64) -> Self {
54        Self {
55            access_token: result.access_token.clone(),
56            refresh_token: result.refresh_token.clone(),
57            expires_in: result.expires_in,
58            obtained_at,
59            scope: result.scope.clone(),
60        }
61    }
62
63    /// Returns the expiration timestamp in Unix milliseconds.
64    #[must_use]
65    pub fn expires_at_ms(&self) -> u64 {
66        self.obtained_at + (self.expires_in * 1000)
67    }
68
69    /// Returns whether the token is expired or near expiry (within 60 seconds).
70    #[must_use]
71    pub fn is_expired(&self, current_time_ms: u64) -> bool {
72        // Consider expired if within 60 seconds of expiry
73        current_time_ms + 60_000 >= self.expires_at_ms()
74    }
75
76    /// Returns whether this is a session-scoped authentication.
77    #[must_use]
78    pub fn is_session_scoped(&self) -> bool {
79        self.scope.starts_with("session:")
80    }
81}
82
83/// Sends an authentication request using client_signature grant type.
84///
85/// This is a helper function used by both initial authentication and re-authentication
86/// after reconnection. It generates the signature and sends auth params via the command channel.
87/// The handler is responsible for generating the request ID.
88///
89/// # Arguments
90///
91/// * `credential` - API credentials for signing the request
92/// * `scope` - Optional scope (e.g., "session:nautilus" for session-based auth)
93/// * `cmd_tx` - Command channel to send the authentication request
94pub fn send_auth_request(
95    credential: &Credential,
96    scope: Option<String>,
97    cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
98) {
99    let timestamp = get_atomic_clock_realtime().get_time_ms();
100    let nonce = UUID4::new().to_string();
101    let signature = credential.sign_ws_auth(timestamp, &nonce, "");
102
103    let auth_params = DeribitAuthParams {
104        grant_type: "client_signature".to_string(),
105        client_id: credential.api_key.to_string(),
106        timestamp,
107        signature,
108        nonce,
109        data: String::new(),
110        scope,
111    };
112
113    if let Ok(auth_params_value) = serde_json::to_value(&auth_params) {
114        let _ = cmd_tx.send(HandlerCommand::Authenticate {
115            auth_params: auth_params_value,
116        });
117    }
118}
119
120/// Spawns a background task to refresh the authentication token before it expires.
121///
122/// The task sleeps until 80% of the token lifetime has passed, then sends a refresh request.
123/// When the refresh succeeds, a new `Authenticated` message will be received, which triggers
124/// another refresh task - creating a continuous refresh cycle.
125pub fn spawn_token_refresh_task(
126    expires_in: u64,
127    refresh_token: String,
128    cmd_tx: tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
129) {
130    // Refresh at 80% of token lifetime to ensure we never expire
131    let refresh_delay_secs = (expires_in as f64 * 0.8) as u64;
132
133    get_runtime().spawn(async move {
134        log::debug!(
135            "Token refresh scheduled in {refresh_delay_secs}s (token expires in {expires_in}s)"
136        );
137        tokio::time::sleep(Duration::from_secs(refresh_delay_secs)).await;
138
139        log::debug!("Refreshing authentication token...");
140        let refresh_params = DeribitRefreshTokenParams {
141            grant_type: "refresh_token".to_string(),
142            refresh_token,
143        };
144
145        if let Ok(auth_params_value) = serde_json::to_value(&refresh_params) {
146            let _ = cmd_tx.send(HandlerCommand::Authenticate {
147                auth_params: auth_params_value,
148            });
149        }
150    });
151}