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