1use crate::{
29 common::enums::{
30 LighterGroupingType, LighterSelfTradeBehaviorMode, LighterSelfTradeEqualityMode,
31 LighterTxType,
32 },
33 signing::{
34 field::Fp,
35 hash::{HASH_OUT, hash_no_pad, hash_two_to_one},
36 },
37};
38
39pub(super) const NB_ATTRIBUTES_PER_TX: usize = 4;
45
46const ATTR_TYPE_INTEGRATOR_ACCOUNT_INDEX: u8 = 1;
47const ATTR_TYPE_INTEGRATOR_TAKER_FEE: u8 = 2;
48const ATTR_TYPE_INTEGRATOR_MAKER_FEE: u8 = 3;
49const ATTR_TYPE_SKIP_NONCE: u8 = 4;
50const ATTR_TYPE_CANCEL_ALL_MARKET_INDEX: u8 = 5;
51const ATTR_TYPE_SELF_TRADE_BEHAVIOR_MODE: u8 = 6;
52const ATTR_TYPE_SELF_TRADE_EQUALITY_MODE: u8 = 7;
53const NIL_MARKET_INDEX: i64 = 255;
54
55#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
60pub struct L2TxAttributes {
61 pub integrator_account_index: u64,
63 pub integrator_taker_fee: u32,
65 pub integrator_maker_fee: u32,
67 pub skip_nonce: u8,
69 pub cancel_all_market_index: Option<i64>,
72 pub self_trade_behavior_mode: Option<LighterSelfTradeBehaviorMode>,
74 pub self_trade_equality_mode: Option<LighterSelfTradeEqualityMode>,
76}
77
78impl L2TxAttributes {
79 #[must_use]
84 pub fn is_empty(&self) -> bool {
85 self.integrator_account_index == 0
86 && self.integrator_taker_fee == 0
87 && self.integrator_maker_fee == 0
88 && self.skip_nonce == 0
89 && self
90 .cancel_all_market_index
91 .is_none_or(|v| v == NIL_MARKET_INDEX)
92 && self
93 .self_trade_behavior_mode
94 .is_none_or(|v| v == LighterSelfTradeBehaviorMode::default())
95 && self
96 .self_trade_equality_mode
97 .is_none_or(|v| v == LighterSelfTradeEqualityMode::default())
98 }
99
100 pub fn validate(&self) -> anyhow::Result<()> {
110 let count = usize::from(self.integrator_account_index != 0)
111 + usize::from(self.integrator_taker_fee != 0)
112 + usize::from(self.integrator_maker_fee != 0)
113 + usize::from(self.skip_nonce != 0)
114 + usize::from(self.cancel_all_market_index.is_some())
115 + usize::from(self.self_trade_behavior_mode.is_some())
116 + usize::from(self.self_trade_equality_mode.is_some());
117 anyhow::ensure!(
118 count <= NB_ATTRIBUTES_PER_TX,
119 "Lighter permits at most {NB_ATTRIBUTES_PER_TX} transaction attributes"
120 );
121
122 if let Some(index) = self.cancel_all_market_index {
123 anyhow::ensure!(
124 (0..1_i64 << 48).contains(&index),
125 "invalid Lighter cancel-all market index {index}"
126 );
127 }
128
129 let has_self_trade = self
130 .self_trade_behavior_mode
131 .is_some_and(|v| v != LighterSelfTradeBehaviorMode::default())
132 || self
133 .self_trade_equality_mode
134 .is_some_and(|v| v != LighterSelfTradeEqualityMode::default());
135 anyhow::ensure!(
136 !has_self_trade || (self.integrator_taker_fee == 0 && self.integrator_maker_fee == 0),
137 "Lighter self-trade modes cannot combine with non-zero integrator fees"
138 );
139 anyhow::ensure!(
140 self.self_trade_behavior_mode != Some(LighterSelfTradeBehaviorMode::Reduce)
141 || self.self_trade_equality_mode
142 != Some(LighterSelfTradeEqualityMode::MasterAccountIndex),
143 "Lighter reduce self-trade behavior cannot combine with master-account equality"
144 );
145 Ok(())
146 }
147
148 pub(super) fn normalized_pairs(&self) -> anyhow::Result<[(u8, u64); NB_ATTRIBUTES_PER_TX]> {
152 self.validate()?;
153 let mut filled = [(0u8, 0u64); NB_ATTRIBUTES_PER_TX];
154 let mut i = 0;
155 let mut push = |ty: u8, val: u64| {
156 if val != 0 {
157 filled[i] = (ty, val);
158 i += 1;
159 }
160 };
161 push(
162 ATTR_TYPE_INTEGRATOR_ACCOUNT_INDEX,
163 self.integrator_account_index,
164 );
165 push(
166 ATTR_TYPE_INTEGRATOR_TAKER_FEE,
167 u64::from(self.integrator_taker_fee),
168 );
169 push(
170 ATTR_TYPE_INTEGRATOR_MAKER_FEE,
171 u64::from(self.integrator_maker_fee),
172 );
173 push(ATTR_TYPE_SKIP_NONCE, u64::from(self.skip_nonce));
174
175 if let Some(index) = self
176 .cancel_all_market_index
177 .filter(|v| *v != NIL_MARKET_INDEX)
178 {
179 filled[i] = (ATTR_TYPE_CANCEL_ALL_MARKET_INDEX, index as u64);
180 i += 1;
181 }
182
183 if let Some(mode) = self
184 .self_trade_behavior_mode
185 .filter(|v| *v != LighterSelfTradeBehaviorMode::default())
186 {
187 filled[i] = (ATTR_TYPE_SELF_TRADE_BEHAVIOR_MODE, u64::from(mode as u8));
188 i += 1;
189 }
190
191 if let Some(mode) = self
192 .self_trade_equality_mode
193 .filter(|v| *v != LighterSelfTradeEqualityMode::default())
194 {
195 filled[i] = (ATTR_TYPE_SELF_TRADE_EQUALITY_MODE, u64::from(mode as u8));
196 }
197
198 Ok(filled)
199 }
200}
201
202#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
204pub struct OrderInfo {
205 pub market_index: i64,
210 pub client_order_index: i64,
212 pub base_amount: i64,
214 pub price: u32,
216 pub is_ask: bool,
218 pub order_type: u8,
220 pub time_in_force: u8,
222 pub reduce_only: bool,
224 pub trigger_price: u32,
226 pub order_expiry: i64,
228}
229
230impl OrderInfo {
231 fn append_body_elements(&self, elems: &mut Vec<Fp>) {
232 elems.push(field_from_i64(self.market_index));
233 elems.push(field_from_i64(self.client_order_index));
234 elems.push(field_from_i64(self.base_amount));
235 elems.push(field_from_u32(self.price));
236 elems.push(field_from_u8(u8::from(self.is_ask)));
237 elems.push(field_from_u8(self.order_type));
238 elems.push(field_from_u8(self.time_in_force));
239 elems.push(field_from_u8(u8::from(self.reduce_only)));
240 elems.push(field_from_u32(self.trigger_price));
241 elems.push(field_from_i64(self.order_expiry));
242 }
243}
244
245#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
251pub struct TxContext {
252 pub account_index: i64,
254 pub api_key_index: u8,
256 pub nonce: i64,
258 pub expired_at: i64,
260}
261
262pub trait LighterTx {
268 fn tx_type(&self) -> LighterTxType;
270
271 fn context(&self) -> TxContext;
273
274 fn attributes(&self) -> L2TxAttributes {
276 L2TxAttributes::default()
277 }
278
279 fn push_body_elements(&self, elems: &mut Vec<Fp>);
282
283 fn hash_elements(&self, chain_id: u32) -> Vec<Fp> {
286 let ctx = self.context();
287 let mut elems = Vec::with_capacity(16);
288 elems.push(field_from_u32(chain_id));
289 elems.push(field_from_u8(self.tx_type() as u8));
290 elems.push(field_from_i64(ctx.nonce));
291 elems.push(field_from_i64(ctx.expired_at));
292 elems.push(field_from_i64(ctx.account_index));
293 elems.push(field_from_u8(ctx.api_key_index));
294 self.push_body_elements(&mut elems);
295 elems
296 }
297}
298
299#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
301pub struct CreateOrderTxInfo {
302 pub context: TxContext,
304 pub order: OrderInfo,
306 pub attributes: L2TxAttributes,
308}
309
310impl LighterTx for CreateOrderTxInfo {
311 fn tx_type(&self) -> LighterTxType {
312 LighterTxType::CreateOrder
313 }
314
315 fn context(&self) -> TxContext {
316 self.context
317 }
318
319 fn attributes(&self) -> L2TxAttributes {
320 self.attributes
321 }
322
323 fn push_body_elements(&self, elems: &mut Vec<Fp>) {
324 self.order.append_body_elements(elems);
325 }
326}
327
328#[derive(Clone, Debug, PartialEq, Eq, Hash)]
333pub struct CreateGroupedOrdersTxInfo {
334 pub context: TxContext,
336 pub grouping_type: LighterGroupingType,
338 pub orders: Vec<OrderInfo>,
340 pub attributes: L2TxAttributes,
342}
343
344impl LighterTx for CreateGroupedOrdersTxInfo {
345 fn tx_type(&self) -> LighterTxType {
346 LighterTxType::CreateGroupedOrders
347 }
348
349 fn context(&self) -> TxContext {
350 self.context
351 }
352
353 fn attributes(&self) -> L2TxAttributes {
354 self.attributes
355 }
356
357 fn push_body_elements(&self, elems: &mut Vec<Fp>) {
358 elems.push(field_from_u8(self.grouping_type as u8));
359 let mut aggregate = [Fp::ZERO; HASH_OUT];
360
361 for (index, order) in self.orders.iter().enumerate() {
362 let mut body = Vec::with_capacity(10);
363 order.append_body_elements(&mut body);
364 let digest = hash_no_pad(&body);
365 aggregate = if index == 0 {
366 digest
367 } else {
368 hash_two_to_one(aggregate, digest)
369 };
370 }
371
372 elems.extend_from_slice(&aggregate);
373 }
374}
375
376#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
380pub struct CancelOrderTxInfo {
381 pub context: TxContext,
383 pub market_index: i64,
385 pub index: i64,
387 pub skip_nonce: u8,
389}
390
391impl LighterTx for CancelOrderTxInfo {
392 fn tx_type(&self) -> LighterTxType {
393 LighterTxType::CancelOrder
394 }
395
396 fn context(&self) -> TxContext {
397 self.context
398 }
399
400 fn attributes(&self) -> L2TxAttributes {
401 L2TxAttributes {
402 skip_nonce: self.skip_nonce,
403 ..Default::default()
404 }
405 }
406
407 fn push_body_elements(&self, elems: &mut Vec<Fp>) {
408 elems.push(field_from_i64(self.market_index));
409 elems.push(field_from_i64(self.index));
410 }
411}
412
413#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
415pub struct ModifyOrderTxInfo {
416 pub context: TxContext,
418 pub market_index: i64,
420 pub index: i64,
422 pub base_amount: i64,
424 pub price: u32,
426 pub trigger_price: u32,
428 pub attributes: L2TxAttributes,
430}
431
432impl LighterTx for ModifyOrderTxInfo {
433 fn tx_type(&self) -> LighterTxType {
434 LighterTxType::ModifyOrder
435 }
436
437 fn context(&self) -> TxContext {
438 self.context
439 }
440
441 fn attributes(&self) -> L2TxAttributes {
442 self.attributes
443 }
444
445 fn push_body_elements(&self, elems: &mut Vec<Fp>) {
446 elems.push(field_from_i64(self.market_index));
447 elems.push(field_from_i64(self.index));
448 elems.push(field_from_i64(self.base_amount));
449 elems.push(field_from_u32(self.price));
450 elems.push(field_from_u32(self.trigger_price));
451 }
452}
453
454#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
459pub struct ApproveIntegratorTxInfo {
460 pub context: TxContext,
462 pub integrator_account_index: i64,
464 pub max_perps_taker_fee: u32,
466 pub max_perps_maker_fee: u32,
468 pub max_spot_taker_fee: u32,
470 pub max_spot_maker_fee: u32,
472 pub approval_expiry: i64,
474 pub skip_nonce: u8,
476}
477
478impl LighterTx for ApproveIntegratorTxInfo {
479 fn tx_type(&self) -> LighterTxType {
480 LighterTxType::ApproveIntegrator
481 }
482
483 fn context(&self) -> TxContext {
484 self.context
485 }
486
487 fn attributes(&self) -> L2TxAttributes {
488 L2TxAttributes {
489 skip_nonce: self.skip_nonce,
490 ..Default::default()
491 }
492 }
493
494 fn push_body_elements(&self, elems: &mut Vec<Fp>) {
495 elems.push(field_from_i64(self.integrator_account_index));
496 elems.push(field_from_u32(self.max_perps_taker_fee));
497 elems.push(field_from_u32(self.max_perps_maker_fee));
498 elems.push(field_from_u32(self.max_spot_taker_fee));
499 elems.push(field_from_u32(self.max_spot_maker_fee));
500 elems.push(field_from_i64(self.approval_expiry));
501 }
502}
503
504#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
516pub struct CancelAllOrdersTxInfo {
517 pub context: TxContext,
519 pub time_in_force: u8,
521 pub scheduled_time_ms: i64,
523 pub skip_nonce: u8,
525}
526
527impl LighterTx for CancelAllOrdersTxInfo {
528 fn tx_type(&self) -> LighterTxType {
529 LighterTxType::CancelAllOrders
530 }
531
532 fn context(&self) -> TxContext {
533 self.context
534 }
535
536 fn attributes(&self) -> L2TxAttributes {
537 L2TxAttributes {
538 skip_nonce: self.skip_nonce,
539 ..Default::default()
540 }
541 }
542
543 fn push_body_elements(&self, elems: &mut Vec<Fp>) {
544 elems.push(field_from_u8(self.time_in_force));
545 elems.push(field_from_i64(self.scheduled_time_ms));
546 }
547}
548
549#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
561pub struct UpdateLeverageTxInfo {
562 pub context: TxContext,
564 pub market_index: i64,
566 pub initial_margin_fraction: u16,
568 pub margin_mode: u8,
570 pub skip_nonce: u8,
572}
573
574impl LighterTx for UpdateLeverageTxInfo {
575 fn tx_type(&self) -> LighterTxType {
576 LighterTxType::UpdateLeverage
577 }
578
579 fn context(&self) -> TxContext {
580 self.context
581 }
582
583 fn attributes(&self) -> L2TxAttributes {
584 L2TxAttributes {
585 skip_nonce: self.skip_nonce,
586 ..Default::default()
587 }
588 }
589
590 fn push_body_elements(&self, elems: &mut Vec<Fp>) {
591 elems.push(field_from_i64(self.market_index));
592 elems.push(field_from_u16(self.initial_margin_fraction));
593 elems.push(field_from_u8(self.margin_mode));
594 }
595}
596
597#[inline]
598fn field_from_u8(v: u8) -> Fp {
599 Fp::from_u64_reduce(u64::from(v))
600}
601
602#[inline]
603fn field_from_u16(v: u16) -> Fp {
604 Fp::from_u64_reduce(u64::from(v))
605}
606
607#[inline]
608fn field_from_u32(v: u32) -> Fp {
609 Fp::from_u64_reduce(u64::from(v))
610}
611
612#[inline]
617fn field_from_i64(v: i64) -> Fp {
618 Fp::from_u64_reduce(v as u64)
619}
620
621#[cfg(test)]
622mod tests {
623 use proptest::prelude::*;
624 use rstest::rstest;
625
626 use super::*;
627
628 fn ctx() -> TxContext {
629 TxContext {
630 account_index: 12_345,
631 api_key_index: 5,
632 nonce: 7,
633 expired_at: 1_777_804_395_089,
634 }
635 }
636
637 #[rstest]
638 fn create_order_preamble_then_body() {
639 let tx = CreateOrderTxInfo {
640 context: ctx(),
641 order: OrderInfo {
642 market_index: 0,
643 client_order_index: 123,
644 base_amount: 1_000,
645 price: 405_000,
646 is_ask: true,
647 order_type: 0,
648 time_in_force: 1,
649 reduce_only: false,
650 trigger_price: 0,
651 order_expiry: 1_735_689_600_000,
652 },
653 attributes: L2TxAttributes::default(),
654 };
655 let elems = tx.hash_elements(300);
656
657 let expected = [
660 field_from_u32(300), field_from_u8(14), field_from_i64(7), field_from_i64(1_777_804_395_089), field_from_i64(12_345), field_from_u8(5), field_from_i64(0), field_from_i64(123), field_from_i64(1_000), field_from_u32(405_000), field_from_u8(1), field_from_u8(0), field_from_u8(1), field_from_u8(0), field_from_u32(0), field_from_i64(1_735_689_600_000), ];
677 assert_eq!(elems.as_slice(), expected.as_slice());
678 }
679
680 #[rstest]
681 fn create_order_preimage_encodes_widened_market_index() {
682 let tx = CreateOrderTxInfo {
683 context: ctx(),
684 order: OrderInfo {
685 market_index: 40_000,
686 client_order_index: 123,
687 base_amount: 1_000,
688 price: 405_000,
689 is_ask: true,
690 order_type: 0,
691 time_in_force: 1,
692 reduce_only: false,
693 trigger_price: 0,
694 order_expiry: 1_735_689_600_000,
695 },
696 attributes: L2TxAttributes::default(),
697 };
698
699 let elems = tx.hash_elements(300);
700
701 assert_eq!(elems.len(), 16);
702 assert_eq!(elems[6], Fp::from_u64_reduce(40_000));
703 }
704
705 #[rstest]
706 fn cancel_order_preamble_then_body() {
707 let tx = CancelOrderTxInfo {
708 context: ctx(),
709 market_index: 0,
710 index: 123,
711 skip_nonce: 0,
712 };
713 let elems = tx.hash_elements(300);
714
715 let expected = [
716 field_from_u32(300), field_from_u8(15), field_from_i64(7), field_from_i64(1_777_804_395_089), field_from_i64(12_345), field_from_u8(5), field_from_i64(0), field_from_i64(123), ];
725 assert_eq!(elems.as_slice(), expected.as_slice());
726 }
727
728 #[rstest]
729 fn modify_order_preamble_then_body() {
730 let tx = ModifyOrderTxInfo {
731 context: ctx(),
732 market_index: 0,
733 index: 123,
734 base_amount: 1_100,
735 price: 410_000,
736 trigger_price: 0,
737 attributes: L2TxAttributes::default(),
738 };
739 let elems = tx.hash_elements(300);
740
741 let expected = [
742 field_from_u32(300), field_from_u8(17), field_from_i64(7), field_from_i64(1_777_804_395_089), field_from_i64(12_345), field_from_u8(5), field_from_i64(0), field_from_i64(123), field_from_i64(1_100), field_from_u32(410_000), field_from_u32(0), ];
754 assert_eq!(elems.as_slice(), expected.as_slice());
755 }
756
757 #[rstest]
758 fn approve_integrator_preamble_then_body() {
759 let tx = ApproveIntegratorTxInfo {
760 context: ctx(),
761 integrator_account_index: 723_813,
762 max_perps_taker_fee: 500,
763 max_perps_maker_fee: 200,
764 max_spot_taker_fee: 600,
765 max_spot_maker_fee: 300,
766 approval_expiry: 1_780_000_000_000,
767 skip_nonce: 0,
768 };
769 let elems = tx.hash_elements(300);
770
771 let expected = [
772 field_from_u32(300), field_from_u8(45), field_from_i64(7), field_from_i64(1_777_804_395_089), field_from_i64(12_345), field_from_u8(5), field_from_i64(723_813), field_from_u32(500), field_from_u32(200), field_from_u32(600), field_from_u32(300), field_from_i64(1_780_000_000_000), ];
785 assert_eq!(elems.as_slice(), expected.as_slice());
786 }
787
788 #[rstest]
789 #[case::immediate(0, 0)]
790 #[case::scheduled(1, 1_800_000_000_000)]
791 #[case::abort_scheduled(2, 0)]
792 fn cancel_all_orders_preamble_then_body(
793 #[case] time_in_force: u8,
794 #[case] scheduled_time_ms: i64,
795 ) {
796 let tx = CancelAllOrdersTxInfo {
797 context: ctx(),
798 time_in_force,
799 scheduled_time_ms,
800 skip_nonce: 0,
801 };
802 let elems = tx.hash_elements(300);
803
804 let expected = [
805 field_from_u32(300), field_from_u8(16), field_from_i64(7), field_from_i64(1_777_804_395_089), field_from_i64(12_345), field_from_u8(5), field_from_u8(time_in_force), field_from_i64(scheduled_time_ms), ];
814 assert_eq!(elems.as_slice(), expected.as_slice());
815 }
816
817 #[rstest]
818 #[case::cross(0)]
819 #[case::isolated(1)]
820 fn update_leverage_preamble_then_body(#[case] margin_mode: u8) {
821 let tx = UpdateLeverageTxInfo {
822 context: ctx(),
823 market_index: 3,
824 initial_margin_fraction: 500, margin_mode,
826 skip_nonce: 0,
827 };
828 let elems = tx.hash_elements(300);
829
830 let expected = [
831 field_from_u32(300), field_from_u8(20), field_from_i64(7), field_from_i64(1_777_804_395_089), field_from_i64(12_345), field_from_u8(5), field_from_i64(3), field_from_u16(500), field_from_u8(margin_mode), ];
841 assert_eq!(elems.as_slice(), expected.as_slice());
842 }
843
844 #[rstest]
845 #[case::default(L2TxAttributes::default(), true)]
846 #[case::integrator_account_only(
847 L2TxAttributes { integrator_account_index: 1, ..Default::default() },
848 false,
849 )]
850 #[case::taker_fee_only(
851 L2TxAttributes { integrator_taker_fee: 1, ..Default::default() },
852 false,
853 )]
854 #[case::maker_fee_only(
855 L2TxAttributes { integrator_maker_fee: 1, ..Default::default() },
856 false,
857 )]
858 #[case::skip_nonce_only(
859 L2TxAttributes { skip_nonce: 1, ..Default::default() },
860 false,
861 )]
862 fn attributes_is_empty_truth_table(#[case] attrs: L2TxAttributes, #[case] expected: bool) {
863 assert_eq!(attrs.is_empty(), expected);
864 }
865
866 #[rstest]
867 #[case::all_empty(L2TxAttributes::default(), [(0, 0); NB_ATTRIBUTES_PER_TX])]
868 #[case::skip_only(
869 L2TxAttributes { skip_nonce: 1, ..Default::default() },
870 [(4, 1), (0, 0), (0, 0), (0, 0)],
871 )]
872 #[case::all_set(
873 L2TxAttributes {
874 integrator_account_index: 100,
875 integrator_taker_fee: 50,
876 integrator_maker_fee: 20,
877 skip_nonce: 1,
878 ..Default::default()
879 },
880 [(1, 100), (2, 50), (3, 20), (4, 1)],
881 )]
882 #[case::sparse_with_padding(
883 L2TxAttributes {
884 integrator_account_index: 723_813,
885 integrator_taker_fee: 0,
886 integrator_maker_fee: 100,
887 skip_nonce: 0,
888 ..Default::default()
889 },
890 [(1, 723_813), (3, 100), (0, 0), (0, 0)],
891 )]
892 #[case::account_and_skip_only(
893 L2TxAttributes {
894 integrator_account_index: 7,
895 integrator_maker_fee: 9,
896 ..Default::default()
897 },
898 [(1, 7), (3, 9), (0, 0), (0, 0)],
899 )]
900 fn normalized_pairs_truth_table(
901 #[case] attrs: L2TxAttributes,
902 #[case] expected: [(u8, u64); NB_ATTRIBUTES_PER_TX],
903 ) {
904 assert_eq!(attrs.normalized_pairs().unwrap(), expected);
905 }
906
907 #[rstest]
908 #[case::market_zero(Some(0), [(5, 0), (0, 0), (0, 0), (0, 0)], false)]
909 #[case::market_other(Some(4098), [(5, 4098), (0, 0), (0, 0), (0, 0)], false)]
910 #[case::market_nil(Some(255), [(0, 0); NB_ATTRIBUTES_PER_TX], true)]
911 #[case::market_absent(None, [(0, 0); NB_ATTRIBUTES_PER_TX], true)]
912 fn cancel_all_market_attribute_normalization(
913 #[case] index: Option<i64>,
914 #[case] expected: [(u8, u64); NB_ATTRIBUTES_PER_TX],
915 #[case] empty: bool,
916 ) {
917 let attrs = L2TxAttributes {
918 cancel_all_market_index: index,
919 ..Default::default()
920 };
921
922 assert_eq!(attrs.normalized_pairs().unwrap(), expected);
923 assert_eq!(attrs.is_empty(), empty);
924 }
925
926 #[rstest]
927 #[case(-1)]
928 #[case(1_i64 << 48)]
929 fn cancel_all_market_attribute_rejects_out_of_range(#[case] index: i64) {
930 let attrs = L2TxAttributes {
931 cancel_all_market_index: Some(index),
932 ..Default::default()
933 };
934
935 assert_eq!(
936 attrs.validate().unwrap_err().to_string(),
937 format!("invalid Lighter cancel-all market index {index}")
938 );
939 }
940
941 #[rstest]
942 #[case(
943 LighterSelfTradeBehaviorMode::ExpireMaker,
944 LighterSelfTradeEqualityMode::AccountIndex,
945 true
946 )]
947 #[case(
948 LighterSelfTradeBehaviorMode::ExpireMaker,
949 LighterSelfTradeEqualityMode::MasterAccountIndex,
950 true
951 )]
952 #[case(
953 LighterSelfTradeBehaviorMode::ExpireTaker,
954 LighterSelfTradeEqualityMode::AccountIndex,
955 true
956 )]
957 #[case(
958 LighterSelfTradeBehaviorMode::ExpireTaker,
959 LighterSelfTradeEqualityMode::MasterAccountIndex,
960 true
961 )]
962 #[case(
963 LighterSelfTradeBehaviorMode::CancelBoth,
964 LighterSelfTradeEqualityMode::AccountIndex,
965 true
966 )]
967 #[case(
968 LighterSelfTradeBehaviorMode::CancelBoth,
969 LighterSelfTradeEqualityMode::MasterAccountIndex,
970 true
971 )]
972 #[case(
973 LighterSelfTradeBehaviorMode::Reduce,
974 LighterSelfTradeEqualityMode::AccountIndex,
975 true
976 )]
977 #[case(
978 LighterSelfTradeBehaviorMode::Reduce,
979 LighterSelfTradeEqualityMode::MasterAccountIndex,
980 false
981 )]
982 fn self_trade_attribute_combinations(
983 #[case] behavior: LighterSelfTradeBehaviorMode,
984 #[case] equality: LighterSelfTradeEqualityMode,
985 #[case] valid: bool,
986 ) {
987 let attrs = L2TxAttributes {
988 integrator_account_index: 723_813,
989 self_trade_behavior_mode: Some(behavior),
990 self_trade_equality_mode: Some(equality),
991 ..Default::default()
992 };
993
994 assert_eq!(attrs.validate().is_ok(), valid);
995 }
996
997 #[rstest]
998 #[case::taker(17, 0)]
999 #[case::maker(0, 29)]
1000 fn self_trade_attribute_rejects_nonzero_fees(
1001 #[case] taker: u32,
1002 #[case] maker: u32,
1003 #[values(false, true)] equality_only: bool,
1004 ) {
1005 let attrs = L2TxAttributes {
1006 integrator_account_index: 723_813,
1007 integrator_taker_fee: taker,
1008 integrator_maker_fee: maker,
1009 self_trade_behavior_mode: (!equality_only)
1010 .then_some(LighterSelfTradeBehaviorMode::ExpireTaker),
1011 self_trade_equality_mode: equality_only
1012 .then_some(LighterSelfTradeEqualityMode::MasterAccountIndex),
1013 ..Default::default()
1014 };
1015
1016 assert_eq!(
1017 attrs.validate().unwrap_err().to_string(),
1018 "Lighter self-trade modes cannot combine with non-zero integrator fees"
1019 );
1020 }
1021
1022 #[rstest]
1023 fn attribute_capacity_counts_explicit_defaults() {
1024 let attrs = L2TxAttributes {
1025 integrator_account_index: 723_813,
1026 skip_nonce: 1,
1027 cancel_all_market_index: Some(255),
1028 self_trade_behavior_mode: Some(LighterSelfTradeBehaviorMode::ExpireMaker),
1029 self_trade_equality_mode: Some(LighterSelfTradeEqualityMode::AccountIndex),
1030 ..Default::default()
1031 };
1032
1033 assert_eq!(
1034 attrs.normalized_pairs().unwrap_err().to_string(),
1035 "Lighter permits at most 4 transaction attributes"
1036 );
1037 }
1038
1039 #[rstest]
1040 fn self_trade_normalization_preserves_attribute_order() {
1041 let attrs = L2TxAttributes {
1042 integrator_account_index: 723_813,
1043 skip_nonce: 1,
1044 self_trade_behavior_mode: Some(LighterSelfTradeBehaviorMode::CancelBoth),
1045 self_trade_equality_mode: Some(LighterSelfTradeEqualityMode::MasterAccountIndex),
1046 ..Default::default()
1047 };
1048
1049 assert_eq!(
1050 attrs.normalized_pairs().unwrap(),
1051 [(1, 723_813), (4, 1), (6, 2), (7, 1)]
1052 );
1053 }
1054
1055 #[rstest]
1056 fn negative_int_field_matches_go_cast() {
1057 let from_neg_one = field_from_i64(-1);
1060 let from_u64_max = Fp::from_u64_reduce(u64::MAX);
1061 assert_eq!(from_neg_one, from_u64_max);
1062 }
1063
1064 proptest! {
1065 #[rstest]
1068 fn prop_field_from_i64_matches_u64_cast(v in any::<i64>()) {
1069 prop_assert_eq!(field_from_i64(v), Fp::from_u64_reduce(v as u64));
1070 }
1071 }
1072}