use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum InitialCursor {
StartFromBeginning,
StartFromNow,
StartFromTimestamp { timestamp: i64 },
}
impl Default for InitialCursor {
fn default() -> Self {
Self::StartFromNow
}
}
impl InitialCursor {
pub fn start_timestamp(&self) -> Option<i64> {
match self {
Self::StartFromBeginning => None,
Self::StartFromNow => Some(chrono::Utc::now().timestamp_millis()),
Self::StartFromTimestamp { timestamp } => Some(*timestamp),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum HyperliquidNetwork {
Mainnet,
Testnet,
Custom { rest_url: String, ws_url: String },
}
impl Default for HyperliquidNetwork {
fn default() -> Self {
Self::Mainnet
}
}
impl HyperliquidNetwork {
pub fn rest_url(&self) -> String {
match self {
Self::Mainnet => "https://api.hyperliquid.xyz/info".to_string(),
Self::Testnet => "https://api.hyperliquid-testnet.xyz/info".to_string(),
Self::Custom { rest_url, .. } => rest_url.clone(),
}
}
pub fn ws_url(&self) -> String {
match self {
Self::Mainnet => "wss://api.hyperliquid.xyz/ws".to_string(),
Self::Testnet => "wss://api.hyperliquid-testnet.xyz/ws".to_string(),
Self::Custom { ws_url, .. } => ws_url.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CoinSelection {
Specific { coins: Vec<String> },
All,
}
impl Default for CoinSelection {
fn default() -> Self {
Self::All
}
}
impl CoinSelection {
pub fn coins(&self) -> Option<&[String]> {
match self {
Self::Specific { coins } => Some(coins.as_slice()),
Self::All => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HyperliquidSourceConfig {
pub network: HyperliquidNetwork,
pub coins: CoinSelection,
pub enable_trades: bool,
pub enable_order_book: bool,
pub enable_mid_prices: bool,
pub enable_funding_rates: bool,
pub enable_liquidations: bool,
pub funding_poll_interval_secs: u64,
pub initial_cursor: InitialCursor,
}
impl Default for HyperliquidSourceConfig {
fn default() -> Self {
Self {
network: HyperliquidNetwork::Mainnet,
coins: CoinSelection::All,
enable_trades: false,
enable_order_book: false,
enable_mid_prices: true,
enable_funding_rates: false,
enable_liquidations: false,
funding_poll_interval_secs: 60,
initial_cursor: InitialCursor::StartFromNow,
}
}
}
impl HyperliquidSourceConfig {
pub fn validate(&self) -> Result<()> {
if let CoinSelection::Specific { coins } = &self.coins {
if coins.is_empty() {
return Err(anyhow!(
"Validation error: coins cannot be empty when using Specific selection"
));
}
}
if self.funding_poll_interval_secs == 0 {
return Err(anyhow!(
"Validation error: funding_poll_interval_secs must be greater than 0"
));
}
if !self.enable_trades
&& !self.enable_order_book
&& !self.enable_mid_prices
&& !self.enable_funding_rates
&& !self.enable_liquidations
{
return Err(anyhow!(
"Validation error: at least one data channel must be enabled"
));
}
Ok(())
}
}