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)]
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
117pub 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(×tamp).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}