Skip to main content

lightcone_sdk/websocket/
auth.rs

1//! Authentication module for Lightcone WebSocket.
2//!
3//! Provides functionality for authenticating with the Lightcone API
4//! to access private user streams (orders, balances, fills).
5//!
6//! # Authentication Flow
7//!
8//! 1. Generate a sign-in message with timestamp
9//! 2. Sign the message with an Ed25519 keypair
10//! 3. POST to the authentication endpoint
11//! 4. Extract token from JSON response
12//! 5. Connect to WebSocket with the auth token
13
14use std::time::{Duration, SystemTime, UNIX_EPOCH};
15
16use ed25519_dalek::{Signer, SigningKey};
17use reqwest::Client;
18use serde::{Deserialize, Serialize};
19
20use crate::websocket::error::{WebSocketError, WsResult};
21
22/// Authentication API base URL
23pub const AUTH_API_URL: &str = "https://tapi.lightcone.xyz/api";
24
25/// Authentication request timeout
26const AUTH_TIMEOUT: Duration = Duration::from_secs(10);
27
28/// Authentication credentials returned after successful login
29#[derive(Debug, Clone)]
30pub struct AuthCredentials {
31    /// The authentication token to use for WebSocket connection
32    pub auth_token: String,
33    /// The user's public key (Base58 encoded)
34    pub user_pubkey: String,
35    /// The user's ID
36    pub user_id: String,
37    /// Token expiration timestamp (Unix seconds)
38    pub expires_at: i64,
39}
40
41/// Request body for login endpoint
42#[derive(Debug, Serialize)]
43struct LoginRequest {
44    /// Raw 32-byte public key
45    pubkey_bytes: Vec<u8>,
46    /// The message that was signed
47    message: String,
48    /// Base58 encoded signature
49    signature_bs58: String,
50}
51
52/// Response from login endpoint
53#[derive(Debug, Deserialize)]
54struct LoginResponse {
55    /// The authentication token
56    token: String,
57    /// The user's ID
58    user_id: String,
59    /// Token expiration timestamp (Unix seconds)
60    expires_at: i64,
61}
62
63/// Generate the sign-in message with current timestamp.
64///
65/// # Returns
66///
67/// The message to be signed, in the format:
68/// ```text
69/// Sign in to Lightcone
70///
71/// Timestamp: {unix_ms}
72/// ```
73///
74/// # Errors
75///
76/// Returns an error if the system time is before the UNIX epoch.
77pub fn generate_signin_message() -> WsResult<String> {
78    let timestamp_ms = SystemTime::now()
79        .duration_since(UNIX_EPOCH)
80        .map_err(|_| WebSocketError::Protocol("System time before UNIX epoch".to_string()))?
81        .as_millis();
82
83    Ok(format!("Sign in to Lightcone\n\nTimestamp: {}", timestamp_ms))
84}
85
86/// Generate the sign-in message with a specific timestamp.
87///
88/// # Arguments
89///
90/// * `timestamp_ms` - Unix timestamp in milliseconds
91///
92/// # Returns
93///
94/// The message to be signed
95pub fn generate_signin_message_with_timestamp(timestamp_ms: u128) -> String {
96    format!("Sign in to Lightcone\n\nTimestamp: {}", timestamp_ms)
97}
98
99/// Authenticate with Lightcone and obtain credentials.
100///
101/// # Arguments
102///
103/// * `signing_key` - The Ed25519 signing key to use for authentication
104///
105/// # Returns
106///
107/// `AuthCredentials` containing the auth token and user public key
108///
109/// # Example
110///
111/// ```ignore
112/// use ed25519_dalek::SigningKey;
113/// use lightcone_sdk::websocket::auth::authenticate;
114///
115/// let signing_key = SigningKey::from_bytes(&secret_key_bytes);
116/// let credentials = authenticate(&signing_key).await?;
117/// println!("Auth token: {}", credentials.auth_token);
118/// ```
119pub async fn authenticate(signing_key: &SigningKey) -> WsResult<AuthCredentials> {
120    // Generate the message
121    let message = generate_signin_message()?;
122
123    // Sign the message
124    let signature = signing_key.sign(message.as_bytes());
125    let signature_b58 = bs58::encode(signature.to_bytes()).into_string();
126
127    // Get the public key
128    let public_key = signing_key.verifying_key();
129    let public_key_b58 = bs58::encode(public_key.to_bytes()).into_string();
130
131    // Create the request body
132    let request = LoginRequest {
133        pubkey_bytes: public_key.to_bytes().to_vec(),
134        message,
135        signature_bs58: signature_b58,
136    };
137
138    // Create client with timeout
139    let client = Client::builder()
140        .timeout(AUTH_TIMEOUT)
141        .build()
142        .map_err(|e| WebSocketError::HttpError(e.to_string()))?;
143
144    // Send the authentication request
145    let url = format!("{}/auth/login_or_register_with_message", AUTH_API_URL);
146    let response = client
147        .post(&url)
148        .json(&request)
149        .send()
150        .await
151        .map_err(|e| WebSocketError::HttpError(e.to_string()))?;
152
153    // Check for HTTP errors
154    if !response.status().is_success() {
155        return Err(WebSocketError::AuthenticationFailed(format!(
156            "HTTP error: {}",
157            response.status()
158        )));
159    }
160
161    // Parse the response body
162    let login_response: LoginResponse = response.json().await.map_err(|e| {
163        WebSocketError::AuthenticationFailed(format!("Failed to parse response: {}", e))
164    })?;
165
166    Ok(AuthCredentials {
167        auth_token: login_response.token,
168        user_pubkey: public_key_b58,
169        user_id: login_response.user_id,
170        expires_at: login_response.expires_at,
171    })
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_generate_signin_message() {
180        let message = generate_signin_message().unwrap();
181        assert!(message.starts_with("Sign in to Lightcone\n\nTimestamp: "));
182    }
183
184    #[test]
185    fn test_generate_signin_message_with_timestamp() {
186        let timestamp = 1234567890123u128;
187        let message = generate_signin_message_with_timestamp(timestamp);
188        assert_eq!(message, "Sign in to Lightcone\n\nTimestamp: 1234567890123");
189    }
190}