use crate::{common::enums::LighterTxType, signing::field::Fp};
pub(super) const NB_ATTRIBUTES_PER_TX: usize = 4;
const ATTR_TYPE_INTEGRATOR_ACCOUNT_INDEX: u8 = 1;
const ATTR_TYPE_INTEGRATOR_TAKER_FEE: u8 = 2;
const ATTR_TYPE_INTEGRATOR_MAKER_FEE: u8 = 3;
const ATTR_TYPE_SKIP_NONCE: u8 = 4;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct L2TxAttributes {
pub integrator_account_index: u64,
pub integrator_taker_fee: u32,
pub integrator_maker_fee: u32,
pub skip_nonce: u8,
}
impl L2TxAttributes {
#[must_use]
pub fn is_empty(&self) -> bool {
self.integrator_account_index == 0
&& self.integrator_taker_fee == 0
&& self.integrator_maker_fee == 0
&& self.skip_nonce == 0
}
pub(super) fn normalized_pairs(&self) -> [(u8, u64); NB_ATTRIBUTES_PER_TX] {
let mut filled = [(0u8, 0u64); NB_ATTRIBUTES_PER_TX];
let mut i = 0;
let mut push = |ty: u8, val: u64| {
if val != 0 {
filled[i] = (ty, val);
i += 1;
}
};
push(
ATTR_TYPE_INTEGRATOR_ACCOUNT_INDEX,
self.integrator_account_index,
);
push(
ATTR_TYPE_INTEGRATOR_TAKER_FEE,
u64::from(self.integrator_taker_fee),
);
push(
ATTR_TYPE_INTEGRATOR_MAKER_FEE,
u64::from(self.integrator_maker_fee),
);
push(ATTR_TYPE_SKIP_NONCE, u64::from(self.skip_nonce));
filled
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct OrderInfo {
pub market_index: i16,
pub client_order_index: i64,
pub base_amount: i64,
pub price: u32,
pub is_ask: bool,
pub order_type: u8,
pub time_in_force: u8,
pub reduce_only: bool,
pub trigger_price: u32,
pub order_expiry: i64,
}
impl OrderInfo {
fn append_body_elements(&self, elems: &mut Vec<Fp>) {
elems.push(field_from_i16(self.market_index));
elems.push(field_from_i64(self.client_order_index));
elems.push(field_from_i64(self.base_amount));
elems.push(field_from_u32(self.price));
elems.push(field_from_u8(u8::from(self.is_ask)));
elems.push(field_from_u8(self.order_type));
elems.push(field_from_u8(self.time_in_force));
elems.push(field_from_u8(u8::from(self.reduce_only)));
elems.push(field_from_u32(self.trigger_price));
elems.push(field_from_i64(self.order_expiry));
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TxContext {
pub account_index: i64,
pub api_key_index: u8,
pub nonce: i64,
pub expired_at: i64,
}
pub trait LighterTx {
fn tx_type(&self) -> LighterTxType;
fn context(&self) -> TxContext;
fn attributes(&self) -> L2TxAttributes {
L2TxAttributes::default()
}
fn push_body_elements(&self, elems: &mut Vec<Fp>);
fn hash_elements(&self, chain_id: u32) -> Vec<Fp> {
let ctx = self.context();
let mut elems = Vec::with_capacity(16);
elems.push(field_from_u32(chain_id));
elems.push(field_from_u8(self.tx_type() as u8));
elems.push(field_from_i64(ctx.nonce));
elems.push(field_from_i64(ctx.expired_at));
elems.push(field_from_i64(ctx.account_index));
elems.push(field_from_u8(ctx.api_key_index));
self.push_body_elements(&mut elems);
elems
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CreateOrderTxInfo {
pub context: TxContext,
pub order: OrderInfo,
pub attributes: L2TxAttributes,
}
impl LighterTx for CreateOrderTxInfo {
fn tx_type(&self) -> LighterTxType {
LighterTxType::CreateOrder
}
fn context(&self) -> TxContext {
self.context
}
fn attributes(&self) -> L2TxAttributes {
self.attributes
}
fn push_body_elements(&self, elems: &mut Vec<Fp>) {
self.order.append_body_elements(elems);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CancelOrderTxInfo {
pub context: TxContext,
pub market_index: i16,
pub index: i64,
pub skip_nonce: u8,
}
impl LighterTx for CancelOrderTxInfo {
fn tx_type(&self) -> LighterTxType {
LighterTxType::CancelOrder
}
fn context(&self) -> TxContext {
self.context
}
fn attributes(&self) -> L2TxAttributes {
L2TxAttributes {
skip_nonce: self.skip_nonce,
..Default::default()
}
}
fn push_body_elements(&self, elems: &mut Vec<Fp>) {
elems.push(field_from_i16(self.market_index));
elems.push(field_from_i64(self.index));
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ModifyOrderTxInfo {
pub context: TxContext,
pub market_index: i16,
pub index: i64,
pub base_amount: i64,
pub price: u32,
pub trigger_price: u32,
pub attributes: L2TxAttributes,
}
impl LighterTx for ModifyOrderTxInfo {
fn tx_type(&self) -> LighterTxType {
LighterTxType::ModifyOrder
}
fn context(&self) -> TxContext {
self.context
}
fn attributes(&self) -> L2TxAttributes {
self.attributes
}
fn push_body_elements(&self, elems: &mut Vec<Fp>) {
elems.push(field_from_i16(self.market_index));
elems.push(field_from_i64(self.index));
elems.push(field_from_i64(self.base_amount));
elems.push(field_from_u32(self.price));
elems.push(field_from_u32(self.trigger_price));
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ApproveIntegratorTxInfo {
pub context: TxContext,
pub integrator_account_index: i64,
pub max_perps_taker_fee: u32,
pub max_perps_maker_fee: u32,
pub max_spot_taker_fee: u32,
pub max_spot_maker_fee: u32,
pub approval_expiry: i64,
pub skip_nonce: u8,
}
impl LighterTx for ApproveIntegratorTxInfo {
fn tx_type(&self) -> LighterTxType {
LighterTxType::ApproveIntegrator
}
fn context(&self) -> TxContext {
self.context
}
fn attributes(&self) -> L2TxAttributes {
L2TxAttributes {
skip_nonce: self.skip_nonce,
..Default::default()
}
}
fn push_body_elements(&self, elems: &mut Vec<Fp>) {
elems.push(field_from_i64(self.integrator_account_index));
elems.push(field_from_u32(self.max_perps_taker_fee));
elems.push(field_from_u32(self.max_perps_maker_fee));
elems.push(field_from_u32(self.max_spot_taker_fee));
elems.push(field_from_u32(self.max_spot_maker_fee));
elems.push(field_from_i64(self.approval_expiry));
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CancelAllOrdersTxInfo {
pub context: TxContext,
pub time_in_force: u8,
pub scheduled_time_ms: i64,
pub skip_nonce: u8,
}
impl LighterTx for CancelAllOrdersTxInfo {
fn tx_type(&self) -> LighterTxType {
LighterTxType::CancelAllOrders
}
fn context(&self) -> TxContext {
self.context
}
fn attributes(&self) -> L2TxAttributes {
L2TxAttributes {
skip_nonce: self.skip_nonce,
..Default::default()
}
}
fn push_body_elements(&self, elems: &mut Vec<Fp>) {
elems.push(field_from_u8(self.time_in_force));
elems.push(field_from_i64(self.scheduled_time_ms));
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct UpdateLeverageTxInfo {
pub context: TxContext,
pub market_index: i16,
pub initial_margin_fraction: u16,
pub margin_mode: u8,
pub skip_nonce: u8,
}
impl LighterTx for UpdateLeverageTxInfo {
fn tx_type(&self) -> LighterTxType {
LighterTxType::UpdateLeverage
}
fn context(&self) -> TxContext {
self.context
}
fn attributes(&self) -> L2TxAttributes {
L2TxAttributes {
skip_nonce: self.skip_nonce,
..Default::default()
}
}
fn push_body_elements(&self, elems: &mut Vec<Fp>) {
elems.push(field_from_i16(self.market_index));
elems.push(field_from_u16(self.initial_margin_fraction));
elems.push(field_from_u8(self.margin_mode));
}
}
#[inline]
fn field_from_u8(v: u8) -> Fp {
Fp::from_u64_reduce(u64::from(v))
}
#[inline]
fn field_from_u16(v: u16) -> Fp {
Fp::from_u64_reduce(u64::from(v))
}
#[inline]
fn field_from_u32(v: u32) -> Fp {
Fp::from_u64_reduce(u64::from(v))
}
#[inline]
fn field_from_i16(v: i16) -> Fp {
field_from_i64(i64::from(v))
}
#[inline]
fn field_from_i64(v: i64) -> Fp {
Fp::from_u64_reduce(v as u64)
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use rstest::rstest;
use super::*;
fn ctx() -> TxContext {
TxContext {
account_index: 12_345,
api_key_index: 5,
nonce: 7,
expired_at: 1_777_804_395_089,
}
}
#[rstest]
fn create_order_preamble_then_body() {
let tx = CreateOrderTxInfo {
context: ctx(),
order: OrderInfo {
market_index: 0,
client_order_index: 123,
base_amount: 1_000,
price: 405_000,
is_ask: true,
order_type: 0,
time_in_force: 1,
reduce_only: false,
trigger_price: 0,
order_expiry: 1_735_689_600_000,
},
attributes: L2TxAttributes::default(),
};
let elems = tx.hash_elements(300);
let expected = [
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_i16(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), ];
assert_eq!(elems.as_slice(), expected.as_slice());
}
#[rstest]
fn cancel_order_preamble_then_body() {
let tx = CancelOrderTxInfo {
context: ctx(),
market_index: 0,
index: 123,
skip_nonce: 0,
};
let elems = tx.hash_elements(300);
let expected = [
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_i16(0), field_from_i64(123), ];
assert_eq!(elems.as_slice(), expected.as_slice());
}
#[rstest]
fn modify_order_preamble_then_body() {
let tx = ModifyOrderTxInfo {
context: ctx(),
market_index: 0,
index: 123,
base_amount: 1_100,
price: 410_000,
trigger_price: 0,
attributes: L2TxAttributes::default(),
};
let elems = tx.hash_elements(300);
let expected = [
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_i16(0), field_from_i64(123), field_from_i64(1_100), field_from_u32(410_000), field_from_u32(0), ];
assert_eq!(elems.as_slice(), expected.as_slice());
}
#[rstest]
fn approve_integrator_preamble_then_body() {
let tx = ApproveIntegratorTxInfo {
context: ctx(),
integrator_account_index: 723_813,
max_perps_taker_fee: 500,
max_perps_maker_fee: 200,
max_spot_taker_fee: 600,
max_spot_maker_fee: 300,
approval_expiry: 1_780_000_000_000,
skip_nonce: 0,
};
let elems = tx.hash_elements(300);
let expected = [
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), ];
assert_eq!(elems.as_slice(), expected.as_slice());
}
#[rstest]
#[case::immediate(0, 0)]
#[case::scheduled(1, 1_800_000_000_000)]
#[case::abort_scheduled(2, 0)]
fn cancel_all_orders_preamble_then_body(
#[case] time_in_force: u8,
#[case] scheduled_time_ms: i64,
) {
let tx = CancelAllOrdersTxInfo {
context: ctx(),
time_in_force,
scheduled_time_ms,
skip_nonce: 0,
};
let elems = tx.hash_elements(300);
let expected = [
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), ];
assert_eq!(elems.as_slice(), expected.as_slice());
}
#[rstest]
#[case::cross(0)]
#[case::isolated(1)]
fn update_leverage_preamble_then_body(#[case] margin_mode: u8) {
let tx = UpdateLeverageTxInfo {
context: ctx(),
market_index: 3,
initial_margin_fraction: 500, margin_mode,
skip_nonce: 0,
};
let elems = tx.hash_elements(300);
let expected = [
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_i16(3), field_from_u16(500), field_from_u8(margin_mode), ];
assert_eq!(elems.as_slice(), expected.as_slice());
}
#[rstest]
#[case::default(L2TxAttributes::default(), true)]
#[case::integrator_account_only(
L2TxAttributes { integrator_account_index: 1, ..Default::default() },
false,
)]
#[case::taker_fee_only(
L2TxAttributes { integrator_taker_fee: 1, ..Default::default() },
false,
)]
#[case::maker_fee_only(
L2TxAttributes { integrator_maker_fee: 1, ..Default::default() },
false,
)]
#[case::skip_nonce_only(
L2TxAttributes { skip_nonce: 1, ..Default::default() },
false,
)]
fn attributes_is_empty_truth_table(#[case] attrs: L2TxAttributes, #[case] expected: bool) {
assert_eq!(attrs.is_empty(), expected);
}
#[rstest]
#[case::all_empty(L2TxAttributes::default(), [(0, 0); NB_ATTRIBUTES_PER_TX])]
#[case::skip_only(
L2TxAttributes { skip_nonce: 1, ..Default::default() },
[(4, 1), (0, 0), (0, 0), (0, 0)],
)]
#[case::all_set(
L2TxAttributes {
integrator_account_index: 100,
integrator_taker_fee: 50,
integrator_maker_fee: 20,
skip_nonce: 1,
},
[(1, 100), (2, 50), (3, 20), (4, 1)],
)]
#[case::sparse_with_padding(
L2TxAttributes {
integrator_account_index: 723_813,
integrator_taker_fee: 0,
integrator_maker_fee: 100,
skip_nonce: 0,
},
[(1, 723_813), (3, 100), (0, 0), (0, 0)],
)]
#[case::account_and_skip_only(
L2TxAttributes {
integrator_account_index: 7,
integrator_maker_fee: 9,
..Default::default()
},
[(1, 7), (3, 9), (0, 0), (0, 0)],
)]
fn normalized_pairs_truth_table(
#[case] attrs: L2TxAttributes,
#[case] expected: [(u8, u64); NB_ATTRIBUTES_PER_TX],
) {
assert_eq!(attrs.normalized_pairs(), expected);
}
#[rstest]
fn negative_int_field_matches_go_cast() {
let from_neg_one = field_from_i64(-1);
let from_u64_max = Fp::from_u64_reduce(u64::MAX);
assert_eq!(from_neg_one, from_u64_max);
}
proptest! {
#[rstest]
fn prop_field_from_i64_matches_u64_cast(v in any::<i64>()) {
prop_assert_eq!(field_from_i64(v), Fp::from_u64_reduce(v as u64));
}
#[rstest]
fn prop_field_from_i16_matches_u64_cast(v in any::<i16>()) {
prop_assert_eq!(field_from_i16(v), Fp::from_u64_reduce(i64::from(v) as u64));
}
}
}