1use derive_builder::Builder;
19use nautilus_core::string::secret::SecretString;
20use serde::{Deserialize, Serialize};
21use zeroize::Zeroize;
22
23use crate::common::enums::{
24 LighterCandleResolution, LighterFundingResolution, LighterOrderBookFilter, LighterTradeType,
25};
26
27#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
28#[serde(rename_all = "snake_case")]
29pub enum LighterTxLookup {
30 Hash,
31 SequenceIndex,
32}
33
34#[derive(Clone, Debug, Deserialize, Serialize, Builder, PartialEq, Eq)]
35pub struct LighterTxQuery {
36 pub by: LighterTxLookup,
37 pub value: String,
38}
39
40#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder, PartialEq, Eq)]
41#[builder(setter(strip_option), default)]
42pub struct LighterOrderBooksQuery {
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub market_id: Option<i64>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub filter: Option<LighterOrderBookFilter>,
47}
48
49#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder, PartialEq, Eq)]
50#[builder(setter(strip_option), default)]
51pub struct LighterOrderBookDetailsQuery {
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub market_id: Option<i64>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub filter: Option<LighterOrderBookFilter>,
56}
57
58#[derive(Clone, Debug, Deserialize, Serialize, Builder, PartialEq, Eq)]
59pub struct LighterOrderBookOrdersQuery {
60 pub market_id: i64,
61 pub limit: u16,
62}
63
64#[derive(Clone, Debug, Deserialize, Serialize, Builder, PartialEq, Eq)]
65pub struct LighterRecentTradesQuery {
66 pub market_id: i64,
67 pub limit: u16,
68}
69
70#[derive(Clone, Debug, Deserialize, Serialize, Builder, PartialEq, Eq)]
71#[builder(setter(strip_option))]
72pub struct LighterCandlesQuery {
73 pub market_id: i64,
74 pub resolution: LighterCandleResolution,
75 pub start_timestamp: i64,
76 pub end_timestamp: i64,
77 pub count_back: i64,
78 #[builder(default)]
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub set_timestamp_to_end: Option<bool>,
81}
82
83#[derive(Clone, Debug, Deserialize, Serialize, Builder, PartialEq, Eq)]
84#[builder(setter(strip_option))]
85pub struct LighterFundingsQuery {
86 pub market_id: i64,
87 pub resolution: LighterFundingResolution,
88 pub start_timestamp: i64,
89 pub end_timestamp: i64,
90 pub count_back: i64,
91}
92
93#[derive(Clone, Debug, Deserialize, Serialize, Builder, PartialEq, Eq)]
94#[builder(setter(strip_option))]
95pub struct LighterTradesQuery {
96 #[builder(default)]
97 #[builder(setter(into, strip_option))]
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub authorization: Option<SecretString>,
100 #[builder(default)]
101 #[builder(setter(into, strip_option))]
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub auth: Option<SecretString>,
104 #[builder(default)]
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub market_id: Option<i64>,
107 #[builder(default)]
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub account_index: Option<i64>,
110 #[builder(default)]
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub order_index: Option<i64>,
113 #[builder(default)]
114 pub sort_by: LighterTradeSortBy,
115 #[builder(default)]
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub sort_dir: Option<LighterSortDirection>,
118 #[builder(default)]
119 #[builder(setter(into, strip_option))]
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub cursor: Option<String>,
122 #[builder(default)]
123 #[serde(rename = "from")]
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub from_timestamp: Option<i64>,
126 #[builder(default)]
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub ask_filter: Option<i8>,
129 #[builder(default)]
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub role: Option<LighterTradeRole>,
132 #[builder(default)]
133 #[serde(rename = "type")]
134 #[serde(skip_serializing_if = "Option::is_none")]
135 pub trade_type: Option<LighterTradeQueryType>,
136 #[builder(default = "100")]
137 pub limit: u16,
138 #[builder(default)]
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub aggregate: Option<bool>,
141}
142
143#[derive(Clone, Debug, Default, Deserialize, Serialize, Builder, PartialEq, Eq)]
144pub struct LighterNextNonceQuery {
145 pub account_index: i64,
146 pub api_key_index: u8,
147}
148
149#[derive(Clone, Debug, Deserialize, Serialize, Builder, PartialEq, Eq)]
150#[builder(setter(strip_option))]
151pub struct LighterMakerOnlyApiKeysQuery {
152 #[builder(default)]
153 #[builder(setter(into, strip_option))]
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub authorization: Option<SecretString>,
156 #[builder(default)]
157 #[builder(setter(into, strip_option))]
158 #[serde(skip_serializing_if = "Option::is_none")]
159 pub auth: Option<SecretString>,
160 pub account_index: i64,
161}
162
163#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
164#[serde(rename_all = "snake_case")]
165pub enum LighterAccountLookup {
166 #[default]
167 Index,
168 L1Address,
169}
170
171#[derive(Clone, Debug, Deserialize, Serialize, Builder, PartialEq, Eq)]
172pub struct LighterAccountQuery {
173 pub by: LighterAccountLookup,
174 pub value: String,
175}
176
177#[derive(Clone, Debug, Deserialize, Serialize, Builder, PartialEq, Eq)]
178#[builder(setter(strip_option))]
179pub struct LighterAccountActiveOrdersQuery {
180 #[builder(default)]
181 #[builder(setter(into, strip_option))]
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub authorization: Option<SecretString>,
184 #[builder(default)]
185 #[builder(setter(into, strip_option))]
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub auth: Option<SecretString>,
188 pub account_index: i64,
189 pub market_id: i64,
190}
191
192#[derive(Clone, Debug, Deserialize, Serialize, Builder, PartialEq, Eq)]
193#[builder(setter(strip_option))]
194pub struct LighterAccountInactiveOrdersQuery {
195 #[builder(default)]
196 #[builder(setter(into, strip_option))]
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub authorization: Option<SecretString>,
199 #[builder(default)]
200 #[builder(setter(into, strip_option))]
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub auth: Option<SecretString>,
203 pub account_index: i64,
204 #[builder(default)]
205 #[serde(skip_serializing_if = "Option::is_none")]
206 pub market_id: Option<i64>,
207 #[builder(default)]
208 #[serde(skip_serializing_if = "Option::is_none")]
209 pub ask_filter: Option<i8>,
210 #[builder(default)]
211 #[builder(setter(into, strip_option))]
212 #[serde(skip_serializing_if = "Option::is_none")]
213 pub between_timestamps: Option<String>,
214 #[builder(default)]
215 #[builder(setter(into, strip_option))]
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub cursor: Option<String>,
218 pub limit: u16,
219}
220
221macro_rules! impl_auth_query_zeroize {
222 ($type:ident) => {
223 impl Zeroize for $type {
224 fn zeroize(&mut self) {
225 self.authorization.zeroize();
226 self.auth.zeroize();
227 }
228 }
229 };
230}
231
232impl_auth_query_zeroize!(LighterTradesQuery);
233impl_auth_query_zeroize!(LighterMakerOnlyApiKeysQuery);
234impl_auth_query_zeroize!(LighterAccountActiveOrdersQuery);
235impl_auth_query_zeroize!(LighterAccountInactiveOrdersQuery);
236
237#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
238#[serde(rename_all = "snake_case")]
239pub enum LighterTradeSortBy {
240 BlockHeight,
241 Timestamp,
242 #[default]
243 TradeId,
244}
245
246#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
247#[serde(rename_all = "lowercase")]
248pub enum LighterSortDirection {
249 #[default]
250 Desc,
251 Asc,
252}
253
254#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
255#[serde(rename_all = "lowercase")]
256pub enum LighterTradeRole {
257 #[default]
258 All,
259 Maker,
260 Taker,
261}
262
263#[derive(Copy, Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
264#[serde(rename_all = "kebab-case")]
265pub enum LighterTradeQueryType {
266 #[default]
267 All,
268 Trade,
269 Liquidation,
270 Deleverage,
271 MarketSettlement,
272}
273
274impl From<LighterTradeType> for LighterTradeQueryType {
275 fn from(value: LighterTradeType) -> Self {
276 match value {
277 LighterTradeType::Trade => Self::Trade,
278 LighterTradeType::Liquidation => Self::Liquidation,
279 LighterTradeType::Deleverage => Self::Deleverage,
280 LighterTradeType::MarketSettlement => Self::MarketSettlement,
281 }
282 }
283}
284
285impl Default for LighterTradesQuery {
286 fn default() -> Self {
287 Self {
288 authorization: None,
289 auth: None,
290 market_id: None,
291 account_index: None,
292 order_index: None,
293 sort_by: LighterTradeSortBy::default(),
294 sort_dir: None,
295 cursor: None,
296 from_timestamp: None,
297 ask_filter: None,
298 role: None,
299 trade_type: None,
300 limit: 100,
301 aggregate: None,
302 }
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use rstest::rstest;
309
310 use super::*;
311
312 #[rstest]
313 fn test_order_books_query_serializes_filter() {
314 let query = LighterOrderBooksQuery {
315 market_id: Some(0),
316 filter: Some(LighterOrderBookFilter::Perp),
317 };
318
319 let value = serde_json::to_value(query).unwrap();
320
321 assert_eq!(value["market_id"], 0);
322 assert_eq!(value["filter"], "perp");
323 }
324
325 #[rstest]
326 fn test_order_book_orders_query_serializes_widened_market_id() {
327 let query = LighterOrderBookOrdersQueryBuilder::default()
328 .market_id(40_000)
329 .limit(10)
330 .build()
331 .unwrap();
332
333 let value = serde_json::to_value(query).unwrap();
334
335 assert_eq!(value["market_id"], 40_000);
336 assert_eq!(value["limit"], 10);
337 }
338
339 #[rstest]
340 fn test_order_book_details_query_serializes_filter() {
341 let query = LighterOrderBookDetailsQueryBuilder::default()
342 .market_id(1)
343 .filter(LighterOrderBookFilter::Spot)
344 .build()
345 .unwrap();
346
347 let value = serde_json::to_value(query).unwrap();
348
349 assert_eq!(value["market_id"], 1);
350 assert_eq!(value["filter"], "spot");
351 }
352
353 #[rstest]
354 fn test_trades_query_renames_type_and_from() {
355 let query = LighterTradesQueryBuilder::default()
356 .market_id(0)
357 .from_timestamp(1_700_000_000_000)
358 .trade_type(LighterTradeQueryType::MarketSettlement)
359 .limit(50)
360 .build()
361 .unwrap();
362
363 let value = serde_json::to_value(query).unwrap();
364
365 assert_eq!(value["sort_by"], "trade_id");
366 assert_eq!(value["from"], 1_700_000_000_000_i64);
367 assert_eq!(value["type"], "market-settlement");
368 assert_eq!(value["limit"], 50);
369 }
370
371 #[rstest]
372 fn test_candles_query_serializes_resolution_and_timestamp_mode() {
373 let query = LighterCandlesQueryBuilder::default()
374 .market_id(0)
375 .resolution(LighterCandleResolution::OneMinute)
376 .start_timestamp(1_700_000_000_000)
377 .end_timestamp(1_700_000_060_000)
378 .count_back(500)
379 .set_timestamp_to_end(false)
380 .build()
381 .unwrap();
382
383 let value = serde_json::to_value(query).unwrap();
384
385 assert_eq!(value["market_id"], 0);
386 assert_eq!(value["resolution"], "1m");
387 assert_eq!(value["start_timestamp"], 1_700_000_000_000_i64);
388 assert_eq!(value["end_timestamp"], 1_700_000_060_000_i64);
389 assert_eq!(value["count_back"], 500);
390 assert_eq!(value["set_timestamp_to_end"], false);
391 }
392
393 #[rstest]
394 fn test_fundings_query_serializes_resolution() {
395 let query = LighterFundingsQueryBuilder::default()
396 .market_id(0)
397 .resolution(LighterFundingResolution::OneHour)
398 .start_timestamp(1_700_000_000_000)
399 .end_timestamp(1_700_003_600_000)
400 .count_back(100)
401 .build()
402 .unwrap();
403
404 let value = serde_json::to_value(query).unwrap();
405
406 assert_eq!(value["market_id"], 0);
407 assert_eq!(value["resolution"], "1h");
408 assert_eq!(value["start_timestamp"], 1_700_000_000_000_i64);
409 assert_eq!(value["end_timestamp"], 1_700_003_600_000_i64);
410 assert_eq!(value["count_back"], 100);
411 }
412
413 #[rstest]
414 fn test_order_book_orders_query_serializes_required_fields() {
415 let query = LighterOrderBookOrdersQuery {
416 market_id: 0,
417 limit: 25,
418 };
419
420 let value = serde_json::to_value(query).unwrap();
421
422 assert_eq!(value["market_id"], 0);
423 assert_eq!(value["limit"], 25);
424 }
425
426 #[rstest]
427 fn test_recent_trades_query_serializes_required_fields() {
428 let query = LighterRecentTradesQueryBuilder::default()
429 .market_id(1)
430 .limit(10)
431 .build()
432 .unwrap();
433
434 let value = serde_json::to_value(query).unwrap();
435
436 assert_eq!(value["market_id"], 1);
437 assert_eq!(value["limit"], 10);
438 }
439
440 #[rstest]
441 fn test_trades_query_serializes_optional_filters() {
442 let mut query = LighterTradesQuery {
443 authorization: Some(SecretString::from("bearer-token")),
444 auth: Some(SecretString::from("auth-token")),
445 market_id: Some(0),
446 account_index: Some(1234),
447 order_index: Some(5678),
448 sort_by: LighterTradeSortBy::Timestamp,
449 sort_dir: Some(LighterSortDirection::Desc),
450 cursor: Some("cursor-1".to_string()),
451 from_timestamp: Some(1_700_000_000_000),
452 ask_filter: Some(1),
453 role: Some(LighterTradeRole::Maker),
454 trade_type: Some(LighterTradeQueryType::from(LighterTradeType::Liquidation)),
455 limit: 75,
456 aggregate: Some(true),
457 };
458
459 let value = serde_json::to_value(&query).unwrap();
460 let debug = format!("{query:?}");
461
462 assert_eq!(value["authorization"], "bearer-token");
463 assert_eq!(value["auth"], "auth-token");
464 assert_eq!(value["market_id"], 0);
465 assert_eq!(value["account_index"], 1234);
466 assert_eq!(value["order_index"], 5678);
467 assert_eq!(value["sort_by"], "timestamp");
468 assert_eq!(value["sort_dir"], "desc");
469 assert_eq!(
470 serde_json::to_value(LighterSortDirection::Asc).unwrap(),
471 "asc",
472 );
473 assert_eq!(value["cursor"], "cursor-1");
474 assert_eq!(value["from"], 1_700_000_000_000_i64);
475 assert_eq!(value["ask_filter"], 1);
476 assert_eq!(value["role"], "maker");
477 assert_eq!(value["type"], "liquidation");
478 assert_eq!(value["limit"], 75);
479 assert_eq!(value["aggregate"], true);
480 assert!(!debug.contains("bearer-token"));
481 assert!(!debug.contains("auth-token"));
482
483 query.zeroize();
484 assert!(query.authorization.is_none());
485 assert!(query.auth.is_none());
486 }
487
488 #[rstest]
489 fn test_account_active_orders_query_serializes_required_fields() {
490 let query = LighterAccountActiveOrdersQueryBuilder::default()
491 .auth("auth-token")
492 .account_index(123)
493 .market_id(0)
494 .build()
495 .unwrap();
496
497 let value = serde_json::to_value(query).unwrap();
498
499 assert_eq!(value["auth"], "auth-token");
500 assert_eq!(value["account_index"], 123);
501 assert_eq!(value["market_id"], 0);
502 assert!(value.get("authorization").is_none());
503 }
504
505 #[rstest]
506 fn test_account_inactive_orders_query_serializes_optional_filters() {
507 let query = LighterAccountInactiveOrdersQueryBuilder::default()
508 .authorization("bearer-token")
509 .account_index(123)
510 .market_id(0)
511 .ask_filter(1)
512 .between_timestamps("1700000000-1700003600")
513 .cursor("cursor-1")
514 .limit(50)
515 .build()
516 .unwrap();
517
518 let value = serde_json::to_value(query).unwrap();
519
520 assert_eq!(value["authorization"], "bearer-token");
521 assert_eq!(value["account_index"], 123);
522 assert_eq!(value["market_id"], 0);
523 assert_eq!(value["ask_filter"], 1);
524 assert_eq!(value["between_timestamps"], "1700000000-1700003600",);
525 assert_eq!(value["cursor"], "cursor-1");
526 assert_eq!(value["limit"], 50);
527 assert!(value.get("auth").is_none());
528 }
529}