Skip to main content

drasi_source_hyperliquid/
descriptor.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//! Hyperliquid source plugin descriptor and configuration DTOs.
16
17use crate::{CoinSelection, HyperliquidNetwork, HyperliquidSourceBuilder, InitialCursor};
18use drasi_plugin_sdk::prelude::*;
19use utoipa::OpenApi;
20
21/// Hyperliquid source configuration DTO.
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
23#[schema(as = source::hyperliquid::HyperliquidSourceConfig)]
24#[serde(rename_all = "camelCase", deny_unknown_fields)]
25pub struct HyperliquidSourceConfigDto {
26    #[serde(default)]
27    #[schema(value_type = source::hyperliquid::HyperliquidNetwork)]
28    pub network: HyperliquidNetworkDto,
29    #[serde(default)]
30    #[schema(value_type = source::hyperliquid::CoinSelection)]
31    pub coins: CoinSelectionDto,
32    #[serde(default = "default_bool_false")]
33    pub enable_trades: ConfigValue<bool>,
34    #[serde(default = "default_bool_false")]
35    pub enable_order_book: ConfigValue<bool>,
36    #[serde(default = "default_bool_true")]
37    pub enable_mid_prices: ConfigValue<bool>,
38    #[serde(default = "default_bool_false")]
39    pub enable_funding_rates: ConfigValue<bool>,
40    #[serde(default = "default_bool_false")]
41    pub enable_liquidations: ConfigValue<bool>,
42    #[serde(default = "default_poll_interval")]
43    pub funding_poll_interval_secs: ConfigValue<u64>,
44    #[serde(default)]
45    #[schema(value_type = source::hyperliquid::InitialCursor)]
46    pub initial_cursor: InitialCursorDto,
47}
48
49fn default_bool_false() -> ConfigValue<bool> {
50    ConfigValue::Static(false)
51}
52
53fn default_bool_true() -> ConfigValue<bool> {
54    ConfigValue::Static(true)
55}
56
57fn default_poll_interval() -> ConfigValue<u64> {
58    ConfigValue::Static(60)
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
62#[schema(as = source::hyperliquid::HyperliquidNetwork)]
63#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
64#[derive(Default)]
65pub enum HyperliquidNetworkDto {
66    #[default]
67    Mainnet,
68    Testnet,
69    Custom {
70        rest_url: ConfigValue<String>,
71        ws_url: ConfigValue<String>,
72    },
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
76#[schema(as = source::hyperliquid::CoinSelection)]
77#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
78#[derive(Default)]
79pub enum CoinSelectionDto {
80    Specific {
81        coins: Vec<String>,
82    },
83    #[default]
84    All,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
88#[schema(as = source::hyperliquid::InitialCursor)]
89#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
90#[derive(Default)]
91pub enum InitialCursorDto {
92    StartFromBeginning,
93    #[default]
94    StartFromNow,
95    StartFromTimestamp {
96        timestamp: ConfigValue<i64>,
97    },
98}
99
100#[derive(OpenApi)]
101#[openapi(components(schemas(
102    HyperliquidSourceConfigDto,
103    HyperliquidNetworkDto,
104    CoinSelectionDto,
105    InitialCursorDto,
106)))]
107struct HyperliquidSchemas;
108
109/// Descriptor for the Hyperliquid source plugin.
110pub struct HyperliquidSourceDescriptor;
111
112#[async_trait]
113impl SourcePluginDescriptor for HyperliquidSourceDescriptor {
114    fn kind(&self) -> &str {
115        "hyperliquid"
116    }
117
118    fn config_version(&self) -> &str {
119        "1.0.0"
120    }
121
122    fn config_schema_name(&self) -> &str {
123        "source.hyperliquid.HyperliquidSourceConfig"
124    }
125
126    fn config_schema_json(&self) -> String {
127        let api = HyperliquidSchemas::openapi();
128        serde_json::to_string(
129            &api.components
130                .as_ref()
131                .expect("OpenAPI components missing")
132                .schemas,
133        )
134        .expect("Failed to serialize config schema")
135    }
136
137    async fn create_source(
138        &self,
139        id: &str,
140        config_json: &serde_json::Value,
141        auto_start: bool,
142    ) -> anyhow::Result<Box<dyn drasi_lib::sources::Source>> {
143        let dto: HyperliquidSourceConfigDto = serde_json::from_value(config_json.clone())?;
144        let mapper = DtoMapper::new();
145
146        let network = match dto.network {
147            HyperliquidNetworkDto::Mainnet => HyperliquidNetwork::Mainnet,
148            HyperliquidNetworkDto::Testnet => HyperliquidNetwork::Testnet,
149            HyperliquidNetworkDto::Custom { rest_url, ws_url } => {
150                let rest_url = mapper.resolve_typed(&rest_url).await?;
151                let ws_url = mapper.resolve_typed(&ws_url).await?;
152                HyperliquidNetwork::Custom { rest_url, ws_url }
153            }
154        };
155
156        let coins = match dto.coins {
157            CoinSelectionDto::Specific { coins } => CoinSelection::Specific { coins },
158            CoinSelectionDto::All => CoinSelection::All,
159        };
160
161        let initial_cursor = match dto.initial_cursor {
162            InitialCursorDto::StartFromBeginning => InitialCursor::StartFromBeginning,
163            InitialCursorDto::StartFromNow => InitialCursor::StartFromNow,
164            InitialCursorDto::StartFromTimestamp { timestamp } => {
165                let ts = mapper.resolve_typed(&timestamp).await?;
166                InitialCursor::StartFromTimestamp { timestamp: ts }
167            }
168        };
169
170        let mut builder = HyperliquidSourceBuilder::new(id)
171            .with_network(network)
172            .with_auto_start(auto_start)
173            .with_funding_poll_interval_secs(
174                mapper
175                    .resolve_typed(&dto.funding_poll_interval_secs)
176                    .await?,
177            )
178            .with_mid_prices(mapper.resolve_typed(&dto.enable_mid_prices).await?)
179            .with_trades(mapper.resolve_typed(&dto.enable_trades).await?)
180            .with_order_book(mapper.resolve_typed(&dto.enable_order_book).await?)
181            .with_liquidations(mapper.resolve_typed(&dto.enable_liquidations).await?)
182            .with_funding_rates(mapper.resolve_typed(&dto.enable_funding_rates).await?);
183
184        builder = match coins {
185            CoinSelection::Specific { coins } => builder.with_coins(coins),
186            CoinSelection::All => builder.with_all_coins(),
187        };
188
189        builder = match initial_cursor {
190            InitialCursor::StartFromBeginning => builder.start_from_beginning(),
191            InitialCursor::StartFromNow => builder.start_from_now(),
192            InitialCursor::StartFromTimestamp { timestamp } => {
193                builder.start_from_timestamp(timestamp)
194            }
195        };
196
197        let source = builder.build()?;
198        Ok(Box::new(source))
199    }
200}