Skip to main content

drasi_source_hyperliquid/
config.rs

1// Copyright 2025 The Drasi Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
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//! Configuration types for the Hyperliquid source.
16
17use anyhow::{anyhow, Result};
18use serde::{Deserialize, Serialize};
19
20/// Initial cursor behavior for the Hyperliquid source.
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22#[serde(tag = "type", rename_all = "snake_case")]
23pub enum InitialCursor {
24    /// Start streaming from the earliest data available (no filtering).
25    StartFromBeginning,
26    /// Start streaming from the moment the source starts (default).
27    StartFromNow,
28    /// Start streaming from a specific timestamp (milliseconds since epoch).
29    StartFromTimestamp { timestamp: i64 },
30}
31
32impl Default for InitialCursor {
33    fn default() -> Self {
34        Self::StartFromNow
35    }
36}
37
38impl InitialCursor {
39    /// Resolve the timestamp filter for streaming.
40    pub fn start_timestamp(&self) -> Option<i64> {
41        match self {
42            Self::StartFromBeginning => None,
43            Self::StartFromNow => Some(chrono::Utc::now().timestamp_millis()),
44            Self::StartFromTimestamp { timestamp } => Some(*timestamp),
45        }
46    }
47}
48
49/// Hyperliquid environment selection.
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51#[serde(rename_all = "snake_case")]
52pub enum HyperliquidNetwork {
53    Mainnet,
54    Testnet,
55    Custom { rest_url: String, ws_url: String },
56}
57
58impl Default for HyperliquidNetwork {
59    fn default() -> Self {
60        Self::Mainnet
61    }
62}
63
64impl HyperliquidNetwork {
65    pub fn rest_url(&self) -> String {
66        match self {
67            Self::Mainnet => "https://api.hyperliquid.xyz/info".to_string(),
68            Self::Testnet => "https://api.hyperliquid-testnet.xyz/info".to_string(),
69            Self::Custom { rest_url, .. } => rest_url.clone(),
70        }
71    }
72
73    pub fn ws_url(&self) -> String {
74        match self {
75            Self::Mainnet => "wss://api.hyperliquid.xyz/ws".to_string(),
76            Self::Testnet => "wss://api.hyperliquid-testnet.xyz/ws".to_string(),
77            Self::Custom { ws_url, .. } => ws_url.clone(),
78        }
79    }
80}
81
82/// Coin selection strategy for per-coin subscriptions.
83#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
84#[serde(tag = "type", rename_all = "snake_case")]
85pub enum CoinSelection {
86    /// Subscribe to a specific list of coins.
87    Specific { coins: Vec<String> },
88    /// Subscribe to all available coins (discovered via metadata).
89    All,
90}
91
92impl Default for CoinSelection {
93    fn default() -> Self {
94        Self::All
95    }
96}
97
98impl CoinSelection {
99    pub fn coins(&self) -> Option<&[String]> {
100        match self {
101            Self::Specific { coins } => Some(coins.as_slice()),
102            Self::All => None,
103        }
104    }
105}
106
107/// Hyperliquid source configuration.
108#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
109pub struct HyperliquidSourceConfig {
110    pub network: HyperliquidNetwork,
111    pub coins: CoinSelection,
112    pub enable_trades: bool,
113    pub enable_order_book: bool,
114    pub enable_mid_prices: bool,
115    pub enable_funding_rates: bool,
116    pub enable_liquidations: bool,
117    pub funding_poll_interval_secs: u64,
118    pub initial_cursor: InitialCursor,
119}
120
121impl Default for HyperliquidSourceConfig {
122    fn default() -> Self {
123        Self {
124            network: HyperliquidNetwork::Mainnet,
125            coins: CoinSelection::All,
126            enable_trades: false,
127            enable_order_book: false,
128            enable_mid_prices: true,
129            enable_funding_rates: false,
130            enable_liquidations: false,
131            funding_poll_interval_secs: 60,
132            initial_cursor: InitialCursor::StartFromNow,
133        }
134    }
135}
136
137impl HyperliquidSourceConfig {
138    /// Validate configuration values.
139    pub fn validate(&self) -> Result<()> {
140        if let CoinSelection::Specific { coins } = &self.coins {
141            if coins.is_empty() {
142                return Err(anyhow!(
143                    "Validation error: coins cannot be empty when using Specific selection"
144                ));
145            }
146        }
147
148        if self.funding_poll_interval_secs == 0 {
149            return Err(anyhow!(
150                "Validation error: funding_poll_interval_secs must be greater than 0"
151            ));
152        }
153
154        if !self.enable_trades
155            && !self.enable_order_book
156            && !self.enable_mid_prices
157            && !self.enable_funding_rates
158            && !self.enable_liquidations
159        {
160            return Err(anyhow!(
161                "Validation error: at least one data channel must be enabled"
162            ));
163        }
164
165        Ok(())
166    }
167}