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)]
64pub enum HyperliquidNetworkDto {
65    Mainnet,
66    Testnet,
67    Custom {
68        rest_url: ConfigValue<String>,
69        ws_url: ConfigValue<String>,
70    },
71}
72
73impl Default for HyperliquidNetworkDto {
74    fn default() -> Self {
75        Self::Mainnet
76    }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
80#[schema(as = source::hyperliquid::CoinSelection)]
81#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
82pub enum CoinSelectionDto {
83    Specific { coins: Vec<String> },
84    All,
85}
86
87impl Default for CoinSelectionDto {
88    fn default() -> Self {
89        Self::All
90    }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
94#[schema(as = source::hyperliquid::InitialCursor)]
95#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
96pub enum InitialCursorDto {
97    StartFromBeginning,
98    StartFromNow,
99    StartFromTimestamp { timestamp: ConfigValue<i64> },
100}
101
102impl Default for InitialCursorDto {
103    fn default() -> Self {
104        Self::StartFromNow
105    }
106}
107
108#[derive(OpenApi)]
109#[openapi(components(schemas(
110    HyperliquidSourceConfigDto,
111    HyperliquidNetworkDto,
112    CoinSelectionDto,
113    InitialCursorDto,
114)))]
115struct HyperliquidSchemas;
116
117/// Descriptor for the Hyperliquid source plugin.
118pub struct HyperliquidSourceDescriptor;
119
120#[async_trait]
121impl SourcePluginDescriptor for HyperliquidSourceDescriptor {
122    fn kind(&self) -> &str {
123        "hyperliquid"
124    }
125
126    fn config_version(&self) -> &str {
127        "1.0.0"
128    }
129
130    fn config_schema_name(&self) -> &str {
131        "source.hyperliquid.HyperliquidSourceConfig"
132    }
133
134    fn config_schema_json(&self) -> String {
135        let api = HyperliquidSchemas::openapi();
136        serde_json::to_string(
137            &api.components
138                .as_ref()
139                .expect("OpenAPI components missing")
140                .schemas,
141        )
142        .expect("Failed to serialize config schema")
143    }
144
145    async fn create_source(
146        &self,
147        id: &str,
148        config_json: &serde_json::Value,
149        auto_start: bool,
150    ) -> anyhow::Result<Box<dyn drasi_lib::sources::Source>> {
151        let dto: HyperliquidSourceConfigDto = serde_json::from_value(config_json.clone())?;
152        let mapper = DtoMapper::new();
153
154        let network = match dto.network {
155            HyperliquidNetworkDto::Mainnet => HyperliquidNetwork::Mainnet,
156            HyperliquidNetworkDto::Testnet => HyperliquidNetwork::Testnet,
157            HyperliquidNetworkDto::Custom { rest_url, ws_url } => {
158                let rest_url = mapper.resolve_typed(&rest_url).await?;
159                let ws_url = mapper.resolve_typed(&ws_url).await?;
160                HyperliquidNetwork::Custom { rest_url, ws_url }
161            }
162        };
163
164        let coins = match dto.coins {
165            CoinSelectionDto::Specific { coins } => CoinSelection::Specific { coins },
166            CoinSelectionDto::All => CoinSelection::All,
167        };
168
169        let initial_cursor = match dto.initial_cursor {
170            InitialCursorDto::StartFromBeginning => InitialCursor::StartFromBeginning,
171            InitialCursorDto::StartFromNow => InitialCursor::StartFromNow,
172            InitialCursorDto::StartFromTimestamp { timestamp } => {
173                let ts = mapper.resolve_typed(&timestamp).await?;
174                InitialCursor::StartFromTimestamp { timestamp: ts }
175            }
176        };
177
178        let mut builder = HyperliquidSourceBuilder::new(id)
179            .with_network(network)
180            .with_auto_start(auto_start)
181            .with_funding_poll_interval_secs(
182                mapper
183                    .resolve_typed(&dto.funding_poll_interval_secs)
184                    .await?,
185            )
186            .with_mid_prices(mapper.resolve_typed(&dto.enable_mid_prices).await?)
187            .with_trades(mapper.resolve_typed(&dto.enable_trades).await?)
188            .with_order_book(mapper.resolve_typed(&dto.enable_order_book).await?)
189            .with_liquidations(mapper.resolve_typed(&dto.enable_liquidations).await?)
190            .with_funding_rates(mapper.resolve_typed(&dto.enable_funding_rates).await?);
191
192        builder = match coins {
193            CoinSelection::Specific { coins } => builder.with_coins(coins),
194            CoinSelection::All => builder.with_all_coins(),
195        };
196
197        builder = match initial_cursor {
198            InitialCursor::StartFromBeginning => builder.start_from_beginning(),
199            InitialCursor::StartFromNow => builder.start_from_now(),
200            InitialCursor::StartFromTimestamp { timestamp } => {
201                builder.start_from_timestamp(timestamp)
202            }
203        };
204
205        let source = builder.build()?;
206        Ok(Box::new(source))
207    }
208}