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