Skip to main content

perpl_sdk/types/
request.rs

1use alloy::primitives::{Bytes, U256};
2use fastnum::{UD64, UD128};
3
4use super::*;
5use crate::{abi::dex::Exchange::OrderDesc, error::DexError, num, state};
6
7/// Type of the order request.
8///
9/// * [`RequestType::OpenLong`] is used to open a long position (or to decrease,
10///   close, or invert a long position). The only restrictions applied are the
11///   user account must have sufficient collateral available.
12/// * [`RequestType::OpenShort`] is used to open a short position (or to
13///   decrease, close, or invert a short position). The only restrictions
14///   applied are the user account must have sufficient collateral available.
15/// * [`RequestType::CloseLong`] is a reduce only order type and can only be
16///   used to close all or part of an existing long position on the perpetual
17///   contract.
18/// * [`RequestType::CloseShort`] is a reduce only order type and can only be
19///   used to close all or part of an existing short position on the perpetual
20///   contract.
21/// * [`RequestType::Cancel`] is used to cancel an existing order on the
22///   perpetual contract's order book.
23/// * [`RequestType::IncreasePositionCollateral`] is an operation to increase
24///   the collateral of an existing position in the event that it has
25///   insufficient margin or the account holder wishes to reduce leverage.
26/// * [`RequestType::Change`] is an operation to change parameters of an
27///   existing order, gas-efficiently.
28#[derive(Clone, Copy, Debug)]
29pub enum RequestType {
30    OpenLong,
31    OpenShort,
32    CloseLong,
33    CloseShort,
34    Cancel,
35    IncreasePositionCollateral,
36    Change,
37}
38
39/// Request to post/modify an order.
40#[derive(Clone, derive_more::Debug)]
41pub struct OrderRequest {
42    request_id: RequestId,
43    perp_id: PerpetualId,
44    r#type: RequestType,
45    order_id: Option<OrderId>,
46    #[debug("{price}")]
47    price: UD64,
48    #[debug("{size}")]
49    size: UD64,
50    expiry_block: Option<u64>,
51    post_only: bool,
52    fill_or_kill: bool,
53    immediate_or_cancel: bool,
54    max_matches: Option<u32>,
55    #[debug("{leverage}")]
56    leverage: UD64,
57    last_exec_block: Option<u64>,
58    amount: Option<UD128>,
59    max_neg_pnl_collat_bps: u16,
60    builder: Option<BuilderAttribution>,
61}
62
63impl OrderRequest {
64    /// Create a new order request with provided parameters.
65    ///
66    /// Provided [`request_id`] is stored as [`client_order_id`] once the order
67    /// gets placed.
68    ///
69    /// Use [`Self::prepare_v2`] to get an [`OrderDesc`] with its order
70    /// extension and then issue transactions with
71    /// [`crate::abi::dex::Exchange::ExchangeInstance::execOrdersV2`] calls, or
72    /// [`Self::prepare`] for the builder-blind V1
73    /// [`crate::abi::dex::Exchange::ExchangeInstance::execOrders`].
74    #[allow(clippy::too_many_arguments)]
75    pub fn new(
76        request_id: RequestId,
77        perp_id: PerpetualId,
78        r#type: RequestType,
79        order_id: Option<OrderId>,
80        price: UD64,
81        size: UD64,
82        expiry_block: Option<u64>,
83        post_only: bool,
84        fill_or_kill: bool,
85        immediate_or_cancel: bool,
86        max_matches: Option<u32>,
87        leverage: UD64,
88        last_exec_block: Option<u64>,
89        amount: Option<UD128>,
90        max_neg_pnl_collat_bps: u16,
91    ) -> Self {
92        Self {
93            request_id,
94            perp_id,
95            r#type,
96            order_id,
97            price,
98            size,
99            expiry_block,
100            post_only,
101            fill_or_kill,
102            immediate_or_cancel,
103            max_matches,
104            leverage,
105            last_exec_block,
106            amount,
107            max_neg_pnl_collat_bps,
108            builder: None,
109        }
110    }
111
112    /// Attributes the order to a builder, which charges its own additive fee on
113    /// the size the order adds.
114    ///
115    /// Only the V2 entrypoints carry attribution: use [`Self::prepare_v2`] to
116    /// get the corresponding order extension envelope. Attribution is *silently
117    /// dropped* by [`Self::prepare`], as the V1 entrypoints have nothing to
118    /// carry it in.
119    pub fn with_builder(mut self, builder: BuilderAttribution) -> Self {
120        self.builder = Some(builder);
121        self
122    }
123
124    /// Builder attribution of the request, if any.
125    pub fn builder(&self) -> Option<BuilderAttribution> { self.builder }
126
127    /// Prepare order request for execution via the V1 entrypoints
128    /// (`execOrder`/`execOrders`), which cannot carry builder attribution.
129    ///
130    /// # Panics
131    ///
132    /// If the perpetual contract of the request is not tracked by `exchange`.
133    pub fn prepare(&self, exchange: &state::Exchange) -> OrderDesc {
134        let perp = exchange
135            .perpetuals()
136            .get(&self.perp_id)
137            .expect("known perpetual");
138        self.to_order_desc(
139            perp.price_converter(),
140            perp.size_converter(),
141            perp.leverage_converter(),
142            Some(exchange.collateral_converter()),
143        )
144    }
145
146    /// Prepare order request for execution via the V2 entrypoints
147    /// (`execOrderV2`/`execOrdersV2`), returning the order descriptor along
148    /// with its order extension envelope.
149    ///
150    /// The envelope is empty for a request without builder attribution, which
151    /// is the V1-identical fast path on-chain. A batch where no order
152    /// carries attribution can omit the `extensions` array entirely.
153    ///
154    /// Fails if the request carries builder attribution the deployed contract
155    /// does not support, or a builder fee rate the contract's decoder would
156    /// reject - which reverts `execOrderV2` and skips the order on the batched
157    /// path.
158    pub fn prepare_v2(&self, exchange: &state::Exchange) -> Result<(OrderDesc, Bytes), DexError> {
159        let perp = exchange
160            .perpetuals()
161            .get(&self.perp_id)
162            .ok_or(DexError::PerpetualNotTracked(self.perp_id))?;
163        let extension = match self.builder {
164            None => Bytes::new(),
165            Some(builder) => {
166                if !exchange.features().builder_attribution() {
167                    return Err(DexError::UnsupportedByContract(
168                        "builder attribution",
169                        exchange.features(),
170                    ));
171                }
172                builder.encode()?
173            },
174        };
175        Ok((
176            self.to_order_desc(
177                perp.price_converter(),
178                perp.size_converter(),
179                perp.leverage_converter(),
180                Some(exchange.collateral_converter()),
181            ),
182            extension,
183        ))
184    }
185
186    /// Order extension envelope of the request, empty without builder
187    /// attribution.
188    pub fn to_order_extension(&self) -> Result<Bytes, OrderExtensionError> {
189        self.builder
190            .map(|builder| builder.encode())
191            .transpose()
192            .map(Option::unwrap_or_default)
193    }
194
195    pub(crate) fn to_order_desc(
196        &self,
197        price_converter: num::Converter,
198        size_converter: num::Converter,
199        leverage_converter: num::Converter,
200        collateral_converter: Option<num::Converter>,
201    ) -> OrderDesc {
202        OrderDesc {
203            orderDescId: U256::from(self.request_id),
204            perpId: U256::from(self.perp_id),
205            orderType: self.r#type as u8,
206            orderId: U256::from(self.order_id.map(|id| id.get()).unwrap_or(0)),
207            pricePNS: price_converter.to_unsigned(self.price),
208            lotLNS: size_converter.to_unsigned(self.size),
209            expiryBlock: U256::from(self.expiry_block.unwrap_or_default()),
210            postOnly: self.post_only,
211            fillOrKill: self.fill_or_kill,
212            immediateOrCancel: self.immediate_or_cancel,
213            maxMatches: U256::from(self.max_matches.unwrap_or_default()),
214            leverageHdths: leverage_converter.to_unsigned(self.leverage),
215            lastExecutionBlock: U256::from(self.last_exec_block.unwrap_or_default()),
216            amountCNS: self
217                .amount
218                .zip(collateral_converter)
219                .map(|(a, conv)| conv.to_unsigned(a))
220                .unwrap_or_default(),
221            maxNegPnlCollatBPS: U256::from(self.max_neg_pnl_collat_bps),
222        }
223    }
224}
225
226impl From<u8> for RequestType {
227    fn from(value: u8) -> Self {
228        match value {
229            0 => RequestType::OpenLong,
230            1 => RequestType::OpenShort,
231            2 => RequestType::CloseLong,
232            3 => RequestType::CloseShort,
233            4 => RequestType::Cancel,
234            5 => RequestType::IncreasePositionCollateral,
235            6 => RequestType::Change,
236            _ => unreachable!(),
237        }
238    }
239}
240
241impl RequestType {
242    /// Returns the order side for this request type, if applicable.
243    ///
244    /// Returns `Some(side)` for order-placing types (OpenLong, OpenShort,
245    /// CloseLong, CloseShort). Returns `None` for Cancel,
246    /// IncreasePositionCollateral, and Change.
247    pub fn try_side(&self) -> Option<OrderSide> {
248        match self {
249            RequestType::OpenLong | RequestType::CloseShort => Some(OrderSide::Bid),
250            RequestType::OpenShort | RequestType::CloseLong => Some(OrderSide::Ask),
251            _ => None,
252        }
253    }
254}
255
256impl From<RequestType> for OrderType {
257    fn from(value: RequestType) -> Self {
258        match value {
259            RequestType::OpenLong => OrderType::OpenLong,
260            RequestType::OpenShort => OrderType::OpenShort,
261            RequestType::CloseLong => OrderType::CloseLong,
262            RequestType::CloseShort => OrderType::CloseShort,
263            _ => unreachable!(),
264        }
265    }
266}