nautilus_deribit/websocket/
auth.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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::{
19    sync::{
20        Arc,
21        atomic::{AtomicU64, Ordering},
22    },
23    time::Duration,
24};
25
26use nautilus_common::live::runtime::get_runtime;
27use nautilus_core::{UUID4, time::get_atomic_clock_realtime};
28
29use super::{
30    handler::HandlerCommand,
31    messages::{DeribitAuthParams, DeribitAuthResult, DeribitRefreshTokenParams},
32};
33use crate::common::{credential::Credential, rpc::DeribitJsonRpcRequest};
34
35/// Default session name for Deribit WebSocket authentication.
36pub const DEFAULT_SESSION_NAME: &str = "nautilus";
37
38/// Authentication state storing OAuth tokens.
39#[derive(Debug, Clone)]
40pub struct AuthState {
41    /// Access token for API requests.
42    pub access_token: String,
43    /// Refresh token for obtaining new access tokens.
44    pub refresh_token: String,
45    /// Token expiration time in seconds from authentication.
46    pub expires_in: u64,
47    /// Timestamp when tokens were obtained (Unix milliseconds).
48    pub obtained_at: u64,
49    /// Scope used for authentication.
50    pub scope: String,
51}
52
53impl AuthState {
54    /// Creates a new [`AuthState`] from an authentication result.
55    #[must_use]
56    pub fn from_auth_result(result: &DeribitAuthResult, obtained_at: u64) -> Self {
57        Self {
58            access_token: result.access_token.clone(),
59            refresh_token: result.refresh_token.clone(),
60            expires_in: result.expires_in,
61            obtained_at,
62            scope: result.scope.clone(),
63        }
64    }
65
66    /// Returns the expiration timestamp in Unix milliseconds.
67    #[must_use]
68    pub fn expires_at_ms(&self) -> u64 {
69        self.obtained_at + (self.expires_in * 1000)
70    }
71
72    /// Returns whether the token is expired or near expiry (within 60 seconds).
73    #[must_use]
74    pub fn is_expired(&self, current_time_ms: u64) -> bool {
75        // Consider expired if within 60 seconds of expiry
76        current_time_ms + 60_000 >= self.expires_at_ms()
77    }
78
79    /// Returns whether this is a session-scoped authentication.
80    #[must_use]
81    pub fn is_session_scoped(&self) -> bool {
82        self.scope.starts_with("session:")
83    }
84}
85
86/// Sends an authentication request using client_signature grant type.
87///
88/// This is a helper function used by both initial authentication and re-authentication
89/// after reconnection. It generates the signature, creates the JSON-RPC request, and
90/// sends it via the command channel.
91///
92/// # Arguments
93///
94/// * `credential` - API credentials for signing the request
95/// * `scope` - Optional scope (e.g., "session:nautilus" for session-based auth)
96/// * `cmd_tx` - Command channel to send the authentication request
97/// * `request_id_counter` - Counter for generating unique request IDs
98pub fn send_auth_request(
99    credential: &Credential,
100    scope: Option<String>,
101    cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
102    request_id_counter: &Arc<AtomicU64>,
103) {
104    let timestamp = get_atomic_clock_realtime().get_time_ms();
105    let nonce = UUID4::new().to_string();
106    let signature = credential.sign_ws_auth(timestamp, &nonce, "");
107
108    let auth_params = DeribitAuthParams {
109        grant_type: "client_signature".to_string(),
110        client_id: credential.api_key.to_string(),
111        timestamp,
112        signature,
113        nonce,
114        data: String::new(),
115        scope,
116    };
117
118    let request_id = request_id_counter.fetch_add(1, Ordering::Relaxed);
119    let request = DeribitJsonRpcRequest::new(request_id, "public/auth", auth_params);
120
121    if let Ok(payload) = serde_json::to_string(&request) {
122        let _ = cmd_tx.send(HandlerCommand::Authenticate { payload });
123    }
124}
125
126/// Spawns a background task to refresh the authentication token before it expires.
127///
128/// The task sleeps until 80% of the token lifetime has passed, then sends a refresh request.
129/// When the refresh succeeds, a new `Authenticated` message will be received, which triggers
130/// another refresh task - creating a continuous refresh cycle.
131pub fn spawn_token_refresh_task(
132    expires_in: u64,
133    refresh_token: String,
134    cmd_tx: tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
135    request_id_counter: Arc<AtomicU64>,
136) {
137    // Refresh at 80% of token lifetime to ensure we never expire
138    let refresh_delay_secs = (expires_in as f64 * 0.8) as u64;
139
140    get_runtime().spawn(async move {
141        tracing::debug!(
142            "Token refresh scheduled in {}s (token expires in {}s)",
143            refresh_delay_secs,
144            expires_in
145        );
146        tokio::time::sleep(Duration::from_secs(refresh_delay_secs)).await;
147
148        tracing::info!("Refreshing authentication token...");
149        let refresh_params = DeribitRefreshTokenParams {
150            grant_type: "refresh_token".to_string(),
151            refresh_token,
152        };
153
154        let request_id = request_id_counter.fetch_add(1, Ordering::Relaxed);
155        let request = DeribitJsonRpcRequest::new(request_id, "public/auth", refresh_params);
156
157        if let Ok(payload) = serde_json::to_string(&request) {
158            let _ = cmd_tx.send(HandlerCommand::Authenticate { payload });
159        }
160    });
161}