drasi_source_hyperliquid/
descriptor.rs1use crate::{CoinSelection, HyperliquidNetwork, HyperliquidSourceBuilder, InitialCursor};
18use drasi_plugin_sdk::prelude::*;
19use utoipa::OpenApi;
20
21#[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
109pub 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(×tamp).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}