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