Skip to main content

nautilus_lighter/http/
query.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
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
16//! Query parameter builders for Lighter REST endpoints.
17
18use 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}