Skip to main content

perpl_sdk/state/
event.rs

1use alloy::primitives::{B256, U256};
2use fastnum::{D64, D256, UD64, UD128};
3
4use super::{ContractVersion, FeeSchedule, account, order, perpetual, position};
5use crate::{
6    abi::dex::Exchange::{OrderRequest, OrderRequestV2},
7    types,
8};
9
10/// Exchange state processing events.
11///
12/// This is a subset of [`crate::abi::dex::Exchange::ExchangeEvents`] covering
13/// all state mutations and order request error responses handled by SDK,
14/// with numeric system conversions applied.
15#[derive(Clone, derive_more::Debug)]
16pub enum StateEvents {
17    /// Account state updated.
18    Account(AccountEvent),
19
20    /// Order request processing error.
21    Error(OrderError),
22
23    /// Exchange state or configuration updated.
24    Exchange(ExchangeEvent),
25
26    /// Order book state updated.
27    Order(OrderEvent),
28
29    /// Perpetual contract state or configuration updated.
30    Perpetual(PerpetualEvent),
31
32    /// Position state updated.
33    Position(PositionEvent),
34
35    /// Trade happened.
36    Trade(types::Trade),
37}
38
39/// Account state mutation event.
40#[derive(Clone, derive_more::Debug)]
41pub struct AccountEvent {
42    /// ID of the affected account.
43    pub account_id: types::AccountId,
44
45    /// ID of the request resulted in this event, if knonw.
46    pub request_id: Option<types::RequestId>,
47
48    /// Type of the event with corresponding details.
49    pub r#type: AccountEventType,
50}
51
52/// Type of account event with corresponding details.
53#[derive(Clone, Copy, derive_more::Debug)]
54pub enum AccountEventType {
55    /// New account created.
56    Created(types::AccountId),
57
58    /// Account frozen/unfrozen.
59    Frozen(bool),
60
61    /// Account balance updated.
62    BalanceUpdated(#[debug("{_0}")] UD128),
63
64    /// Account locked balance updated.
65    LockedBalanceUpdated(#[debug("{_0}")] UD128),
66
67    /// Account fee tier updated, taking effect on the account's next fill.
68    /// The tier indexes the [`FeeSchedule`] of every contract the account
69    /// trades.
70    FeeTierUpdated(types::FeeTier),
71}
72
73/// Order request processing error with corresponding reason
74#[derive(Clone, derive_more::Debug)]
75pub struct OrderError {
76    /// ID of the perpetual contract of the order.
77    pub perpetual_id: types::PerpetualId,
78
79    /// ID of the account issued the order.
80    pub account_id: types::AccountId,
81
82    /// ID of the request resulted in this event.
83    pub request_id: types::RequestId,
84
85    /// ID of the order the request was targeted at, if known.
86    pub order_id: Option<types::OrderId>,
87
88    /// Failure reason with corresponding details.
89    pub r#type: OrderErrorType,
90}
91
92/// Type of order request failure with corresponding details.
93#[derive(Clone, Copy, derive_more::Debug)]
94pub enum OrderErrorType {
95    /// Account is frozen.
96    AccountFrozen,
97
98    /// Required amount exceeds available balance.
99    AmountExceedsAvailableBalance(#[debug("{_0}")] UD128, #[debug("{_1}")] UD128),
100
101    /// Existing close orders mismatch the actual position type and
102    /// need to be cancelled before issuing new close orders.
103    CancelExistingInvalidCloseOrders,
104
105    /// Close orders can not be changed.
106    CantChangeCloseOrder,
107
108    /// Provide new expiration to change expired order.
109    ChangeExpiredOrderNeedsNewExpiry,
110
111    /// Close order size exceeds position size.
112    CloseOrderExceedsPosition,
113
114    /// Close order side mismatches position type.
115    CloseOrderPositionMismatch,
116
117    /// Perpetual contract is not operational.
118    ContractNotOperational,
119
120    /// Post-only order crosses the book.
121    CrossesBook,
122
123    /// Current block exceeds last execution block specified for the order.
124    ExceedsLastExecutionBlock,
125
126    /// Immediate-or-cancel order was not completely filled.
127    ImmediateOrCancelExecuted,
128
129    /// Available account balance can not cover recycling fee payment.
130    InsuficientFundsForRecycleFee,
131
132    /// Current block exceeds expiration block specified for the order.
133    InvalidExpiryBlock,
134
135    /// Specified order ID is out of range.
136    InvalidOrderId,
137
138    /// Failed to settle maker order.
139    MakerOrderSettlementFailed,
140
141    /// Maximum number of matches reached for the taker order.
142    MaxMatchesReached,
143
144    /// Account reached limit of orders to post.
145    MaximumAccountOrders,
146
147    /// Order does not exist.
148    OrderDoesNotExist,
149
150    /// Builder-code order extension failed a recoverable decode check (unknown
151    /// envelope version, or a builder field out of range) on a batched or
152    /// forwarded V2 request, so this single order was skipped while the rest of
153    /// the batch proceeded.
154    OrderExtensionRejected,
155
156    /// Order posting failed with status.
157    OrderPostFailed(u16),
158
159    /// Settlement of the order will render perpetual contract insolvent.
160    OrderSettlementImpliesInsolvent,
161
162    /// Size of close order exceeds remaining position size.
163    OrderSizeExceedsAvailableSize,
164
165    /// Order to be posted is under minimum amount.
166    PostOrderUnderMinimum,
167
168    /// Specified order price is out of range.
169    PriceOutOfRange,
170
171    /// Specified order size is out of range.
172    SizeOutOfRange,
173
174    /// Maximum PnL slippage value exceeds maximum of 65535
175    ValueExceedsMaximum,
176
177    /// Another account owns the order.
178    WrongAccountForOrder,
179}
180
181// A `FeeSchedule` carries 16 rates, dwarfing the scalar variants. Boxing it
182// would only move an allocation onto a path that is not hot - fee changes are
183// rare - at the cost of every consumer's pattern match.
184#[allow(clippy::large_enum_variant)]
185#[derive(Clone, derive_more::Debug)]
186pub enum ExchangeEvent {
187    /// Deployed contract version stamped by an upgrade, along with the feature
188    /// set it resolves to.
189    ContractVersionUpdated(ContractVersion),
190
191    /// A fee schedule in [`super::Exchange::fee_schedules`] was rewritten,
192    /// under the key it is registered by ([`FeeSchedule::key`]).
193    ///
194    /// Every tracked perpetual contract *currently pointing at* that schedule
195    /// gets a corresponding [`PerpetualEventType::FeeScheduleUpdated`];
196    /// rewriting a schedule never repoints a contract at it, only
197    /// `PerpFeeSchedIdSet` does.
198    FeeScheduleUpdated(FeeSchedule),
199
200    /// Exchange halted/unhalted.
201    Halted(bool),
202
203    /// Minimal posting amount updated.
204    MinPostUpdated(#[debug("{_0}")] UD128),
205
206    /// Minimal settlement amount updated.
207    MinSettleUpdated(#[debug("{_0}")] UD128),
208
209    /// Recycling fee updated.
210    RecycleFeeUpdated(#[debug("{_0}")] UD128),
211}
212
213/// Order book state mutation event.
214#[derive(Clone, derive_more::Debug)]
215pub struct OrderEvent {
216    /// ID of the perpetual contract of the order.
217    pub perpetual_id: types::PerpetualId,
218
219    /// ID of the account issued the order.
220    pub account_id: types::AccountId,
221
222    /// ID of the request resulted in this event, if knonw.
223    pub request_id: Option<types::RequestId>,
224
225    /// Client order ID, if knonw.
226    pub client_order_id: Option<types::RequestId>,
227
228    /// ID of the order affected, if knonw.
229    pub order_id: Option<types::OrderId>,
230
231    /// Builder the order is attributed to, with the fee rate it charges, if
232    /// any.
233    ///
234    /// Available on contract v1.1.7.4+ for orders whose placement was observed
235    /// in the event stream or recovered from the initial snapshot.
236    pub builder: Option<types::BuilderAttribution>,
237
238    /// Type of the event with corresponding details.
239    pub r#type: OrderEventType,
240}
241
242/// Type of order event with corresponding details.
243#[derive(Clone, Copy, derive_more::Debug)]
244pub enum OrderEventType {
245    /// Order filled.
246    /// For maker orders this event is paired with [`OrderEventType::Updated`]
247    /// or [`OrderEventType::Removed`].
248    Filled {
249        #[debug("{fill_price}")]
250        fill_price: UD64,
251        #[debug("{fill_size}")]
252        fill_size: UD64,
253        /// Total fee the fill charged, builder share included.
254        ///
255        /// From contract v1.1.7.5 EVERY fill that changes a position's size is
256        /// charged, in either direction: a close or decrease pays on the
257        /// removed notional at the exit price, netted from the exit
258        /// proceeds rather than debited, and an inverting order pays on
259        /// its full lot. Earlier releases charged additions only, so a
260        /// close fill reports zero there. Liquidation, ADL,
261        /// force-close, unwind, frozen-account close and
262        /// buy-to-liquidate stay uncharged at any version.
263        #[debug("{fee}")]
264        fee: UD64, // Precision of SC calculations is limited to 5 decimals.
265        /// Portion of `fee` earned by the builder the order is attributed to,
266        /// routed entirely to the protocol balance and paid out off-chain.
267        ///
268        /// Included in `fee`, so consumers must not add it on top. Zero on
269        /// liquidation fills and on contracts without builder attribution, and
270        /// on close/decrease fills before v1.1.7.5 - a non-zero
271        /// [`OrderEvent::builder`] does not imply a non-zero fee here.
272        #[debug("{builder_fee}")]
273        builder_fee: UD64,
274        is_maker: bool,
275    },
276
277    /// Order placed to the book.
278    Placed {
279        r#type: types::OrderType,
280        #[debug("{price}")]
281        price: UD64,
282        #[debug("{size}")]
283        size: UD64,
284        expiry_block: u64,
285        #[debug("{leverage}")]
286        leverage: UD64,
287        post_only: bool,
288        fill_or_kill: bool,
289        immediate_or_cancel: bool,
290    },
291
292    /// Order removed from the book.
293    Removed,
294
295    /// Order in the book updated.
296    Updated {
297        #[debug("{:?}", price.map(|v| format!("{v}")))]
298        price: Option<UD64>,
299        #[debug("{:?}", size.map(|v| format!("{v}")))]
300        size: Option<UD64>,
301        expiry_block: Option<u64>,
302    },
303}
304
305/// Perpetual contract state or configuration mutation event.
306#[derive(Clone, derive_more::Debug)]
307pub struct PerpetualEvent {
308    /// ID of the affected perpetual contract.
309    pub perpetual_id: types::PerpetualId,
310
311    /// Type of the event with corresponding details.
312    pub r#type: PerpetualEventType,
313}
314
315/// Type of perpetual event with corresponding details.
316// See the note on `ExchangeEvent` about the `FeeSchedule` variant's size.
317#[allow(clippy::large_enum_variant)]
318#[derive(Clone, Copy, derive_more::Debug)]
319pub enum PerpetualEventType {
320    /// Perpetual contract being added
321    Added,
322
323    /// Funding event occured and rate updated.
324    FundingEvent {
325        #[debug("{rate}")]
326        rate: D64,
327        #[debug("{payment_per_unit}")]
328        payment_per_unit: D256,
329    },
330
331    /// Fee schedule of the contract updated, taking effect on its next fill.
332    ///
333    /// Emitted when the contract's own schedule is rewritten, when it is
334    /// repointed at another schedule ([`FeeSchedule::key`] changed), and when
335    /// the exchange-wide schedule it points at is rewritten.
336    FeeScheduleUpdated(FeeSchedule),
337
338    /// Funding sum scaling exponent updated. The exponent `e` defines the
339    /// divider `10^e` applied when interpreting on-chain funding sums and
340    /// per-unit funding payments for premium PnL calculations.
341    FundingSumScalingExpUpdated(u8),
342
343    /// Initial margin requirement updated.
344    InitialMarginFractionUpdated(#[debug("{_0}")] UD64),
345
346    /// Last price updated.
347    LastPriceUpdated(#[debug("{_0}")] UD64),
348
349    /// Maintenance margin requirement updated.
350    MaintenanceMarginFractionUpdated(#[debug("{_0}")] UD64),
351
352    /// Mark price updated.
353    MarkPriceUpdated(#[debug("{_0}")] UD64),
354
355    /// Base (fee tier 0) maker fee updated.
356    ///
357    /// Deprecated: only replayed from pre-v1.1.7.4 history, where fees were a
358    /// single per-contract pair. Current contracts report every fee change as
359    /// [`PerpetualEventType::FeeScheduleUpdated`].
360    MakerFeeUpdated(#[debug("{_0}")] UD64),
361
362    /// Open interest updated.
363    OpenInterestUpdated(#[debug("{_0}")] UD128),
364
365    /// Oracle configuration updated.
366    OracleConfigurationUpdated { is_used: bool, feed_id: B256 },
367
368    /// Oracle price updated.
369    OraclePriceUpdated(#[debug("{_0}")] UD64),
370
371    /// Perpetual contract paused/unpaused.
372    Paused(bool),
373
374    /// Base (fee tier 0) taker fee updated.
375    ///
376    /// Deprecated, see [`PerpetualEventType::MakerFeeUpdated`].
377    TakerFeeUpdated(#[debug("{_0}")] UD64),
378}
379
380/// Position state mutation event.
381#[derive(Clone, derive_more::Debug)]
382pub struct PositionEvent {
383    /// ID of the perpetual contract of the position.
384    pub perpetual_id: types::PerpetualId,
385
386    /// ID of the account holding the position.
387    pub account_id: types::AccountId,
388
389    /// ID of the order request resulted in this event,
390    /// if applicable.
391    pub request_id: Option<types::RequestId>,
392
393    /// Type of the event with corresponding details.
394    pub r#type: PositionEventType,
395}
396
397/// Type of position event with corresponding details.
398#[derive(Clone, Copy, derive_more::Debug)]
399pub enum PositionEventType {
400    /// Position closed.
401    Closed {
402        r#type: position::PositionType,
403        #[debug("{entry_price}")]
404        entry_price: UD64,
405        #[debug("{exit_price}")]
406        exit_price: UD64,
407        #[debug("{size}")]
408        size: UD64,
409        #[debug("{delta_pnl}")]
410        delta_pnl: D256,
411        #[debug("{premium_pnl}")]
412        premium_pnl: D256,
413    },
414
415    /// Position collateral decreased.
416    CollateralDecreased {
417        #[debug("{prev_entry_price}")]
418        prev_entry_price: UD64,
419        #[debug("{new_entry_price}")]
420        new_entry_price: UD64,
421        #[debug("{deposit}")]
422        deposit: UD128,
423    },
424
425    /// Position decreased.
426    Decreased {
427        #[debug("{prev_size}")]
428        prev_size: UD64,
429        #[debug("{new_size}")]
430        new_size: UD64,
431        #[debug("{deposit}")]
432        deposit: UD128,
433        #[debug("{delta_pnl}")]
434        delta_pnl: D256,
435        #[debug("{premium_pnl}")]
436        premium_pnl: D256,
437    },
438
439    /// Position deleveraged.
440    Deleveraged {
441        force_close: bool,
442        r#type: position::PositionType,
443        #[debug("{entry_price}")]
444        entry_price: UD64,
445        #[debug("{exit_price}")]
446        exit_price: UD64,
447        #[debug("{prev_size}")]
448        prev_size: UD64,
449        #[debug("{new_size}")]
450        new_size: UD64,
451        #[debug("{deposit}")]
452        deposit: UD128,
453        #[debug("{delta_pnl}")]
454        delta_pnl: D256,
455        #[debug("{premium_pnl}")]
456        premium_pnl: D256,
457    },
458
459    /// Position deposit(collateral) updated.
460    DepositUpdated(#[debug("{_0}")] UD128),
461
462    /// Position increased.
463    Increased {
464        #[debug("{entry_price}")]
465        entry_price: UD64,
466        #[debug("{prev_size}")]
467        prev_size: UD64,
468        #[debug("{new_size}")]
469        new_size: UD64,
470        #[debug("{deposit}")]
471        deposit: UD128,
472    },
473
474    /// Position inverted.
475    Inverted {
476        r#type: position::PositionType,
477        #[debug("{entry_price}")]
478        entry_price: UD64,
479        #[debug("{prev_size}")]
480        prev_size: UD64,
481        #[debug("{new_size}")]
482        new_size: UD64,
483        #[debug("{deposit}")]
484        deposit: UD128,
485        #[debug("{delta_pnl}")]
486        delta_pnl: D256,
487        #[debug("{premium_pnl}")]
488        premium_pnl: D256,
489    },
490
491    /// Position liquidated.
492    Liquidated {
493        r#type: position::PositionType,
494        #[debug("{entry_price}")]
495        entry_price: UD64,
496        #[debug("{exit_price}")]
497        exit_price: UD64,
498        #[debug("{prev_size}")]
499        prev_size: UD64,
500        #[debug("{liquidated_size}")]
501        liquidated_size: UD64,
502        #[debug("{new_size}")]
503        new_size: UD64,
504        #[debug("{deposit}")]
505        deposit: UD128,
506        #[debug("{delta_pnl}")]
507        delta_pnl: D256,
508        #[debug("{premium_pnl}")]
509        premium_pnl: D256,
510    },
511
512    /// Position maintenance margin requirement updated due
513    /// to updated maintenane margin fraction.
514    MaintenanceMarginUpdated(#[debug("{_0}")] UD128),
515
516    /// Position opened.
517    Opened {
518        r#type: position::PositionType,
519        #[debug("{entry_price}")]
520        entry_price: UD64,
521        #[debug("{size}")]
522        size: UD64,
523        #[debug("{deposit}")]
524        deposit: UD128,
525    },
526
527    /// Position unrealized PnL updated.
528    UnrealizedPnLUpdated {
529        #[debug("{pnl}")]
530        pnl: D256,
531        #[debug("{delta_pnl}")]
532        delta_pnl: D256,
533        #[debug("{premium_pnl}")]
534        premium_pnl: D256,
535    },
536
537    /// Position unwound.
538    Unwound {
539        r#type: position::PositionType,
540        #[debug("{entry_price}")]
541        entry_price: UD64,
542        #[debug("{exit_price}")]
543        exit_price: UD64,
544        #[debug("{size}")]
545        size: UD64,
546        #[debug("{fair_market_value}")]
547        fair_market_value: D256,
548        #[debug("{payment}")]
549        payment: UD128,
550    },
551}
552
553impl StateEvents {
554    pub fn as_account_event(&self) -> Option<AccountEvent> {
555        if let StateEvents::Account(account_event) = self {
556            Some(account_event.clone())
557        } else {
558            None
559        }
560    }
561
562    pub fn as_error(&self) -> Option<OrderError> {
563        if let StateEvents::Error(error_event) = self { Some(error_event.clone()) } else { None }
564    }
565
566    pub fn as_order_event(&self) -> Option<OrderEvent> {
567        if let StateEvents::Order(order_event) = self { Some(order_event.clone()) } else { None }
568    }
569
570    pub fn as_exchange_event(&self) -> Option<ExchangeEvent> {
571        if let StateEvents::Exchange(exchange_event) = self {
572            Some(exchange_event.clone())
573        } else {
574            None
575        }
576    }
577
578    pub fn as_perpetual_event(&self) -> Option<PerpetualEvent> {
579        if let StateEvents::Perpetual(perpetual_event) = self {
580            Some(perpetual_event.clone())
581        } else {
582            None
583        }
584    }
585
586    pub fn as_position_event(&self) -> Option<PositionEvent> {
587        if let StateEvents::Position(position_event) = self {
588            Some(position_event.clone())
589        } else {
590            None
591        }
592    }
593
594    pub fn as_trade(&self) -> Option<types::Trade> {
595        if let StateEvents::Trade(trade) = self { Some(trade.clone()) } else { None }
596    }
597
598    pub(crate) fn account(
599        acc: &account::Account,
600        ctx: &Option<OrderContext>,
601        r#type: AccountEventType,
602    ) -> Self {
603        Self::Account(AccountEvent {
604            account_id: acc.id(),
605            request_id: ctx.as_ref().map(|c| c.request_id),
606            r#type,
607        })
608    }
609
610    pub(crate) fn order(
611        perp: &perpetual::Perpetual,
612        ord: &order::Order,
613        ctx: &Option<OrderContext>,
614        r#type: OrderEventType,
615    ) -> Self {
616        Self::Order(OrderEvent {
617            perpetual_id: perp.id(),
618            account_id: ord.account_id(),
619            request_id: ctx.as_ref().map(|c| c.request_id),
620            client_order_id: ord.client_order_id(),
621            order_id: Some(ord.order_id()),
622            builder: ord.builder(),
623            r#type,
624        })
625    }
626
627    pub(crate) fn order_error(ctx: &OrderContext, r#type: OrderErrorType) -> StateEvents {
628        Self::Error(OrderError {
629            perpetual_id: ctx.perpetual_id,
630            account_id: ctx.account_id,
631            request_id: ctx.request_id,
632            order_id: ctx.order_id,
633            r#type,
634        })
635    }
636
637    pub(crate) fn affected_order_error(
638        ctx: &OrderContext,
639        ord: &order::Order,
640        r#type: OrderErrorType,
641    ) -> StateEvents {
642        Self::Error(OrderError {
643            perpetual_id: ctx.perpetual_id,
644            account_id: ord.account_id(),
645            request_id: ctx.request_id,
646            order_id: Some(ord.order_id()),
647            r#type,
648        })
649    }
650
651    pub(crate) fn perpetual(
652        perp: &perpetual::Perpetual,
653        r#type: PerpetualEventType,
654    ) -> StateEvents {
655        Self::Perpetual(PerpetualEvent { perpetual_id: perp.id(), r#type })
656    }
657
658    pub(crate) fn position(
659        pos: &position::Position,
660        ctx: &Option<OrderContext>,
661        r#type: PositionEventType,
662    ) -> Self {
663        Self::Position(PositionEvent {
664            perpetual_id: pos.perpetual_id(),
665            account_id: pos.account_id(),
666            request_id: ctx.as_ref().map(|c| c.request_id),
667            r#type,
668        })
669    }
670
671    pub(crate) fn trade(ctx: &OrderContext, taker_fee: UD64, taker_builder_fee: UD64) -> Self {
672        Self::Trade(types::Trade {
673            perpetual_id: ctx.perpetual_id,
674            taker_account_id: ctx.account_id,
675            taker_request_id: ctx.request_id,
676            taker_side: ctx.r#type.try_side().expect("order type with side"),
677            taker_fee,
678            taker_builder: ctx.builder,
679            taker_builder_fee,
680            maker_fills: ctx.maker_fills.clone(),
681        })
682    }
683}
684
685/// Order request context.
686#[derive(Debug)]
687pub(crate) struct OrderContext {
688    pub(crate) perpetual_id: types::PerpetualId,
689    pub(crate) account_id: types::AccountId,
690    pub(crate) request_id: types::RequestId,
691    pub(crate) order_id: Option<types::OrderId>,
692    pub(crate) r#type: types::RequestType,
693    pub(crate) price: U256,
694    pub(crate) expiry_block: u64,
695    pub(crate) leverage: U256,
696    pub(crate) post_only: bool,
697    pub(crate) fill_or_kill: bool,
698    pub(crate) immediate_or_cancel: bool,
699    pub(crate) builder: Option<types::BuilderAttribution>,
700    pub(crate) maker_fills: Vec<types::MakerFill>,
701    pub(crate) clearing_remaining_order: bool,
702    pub(crate) position_closed_at_log_index: Option<u64>,
703}
704
705/// Order ID of a request, `None` for trigger order requests - their order IDs
706/// might exceed `u16::MAX` and they are not supported by the SDK yet.
707fn request_order_id(order_id: U256) -> Option<types::OrderId> {
708    if order_id <= U256::from(u16::MAX) {
709        std::num::NonZeroU16::new(order_id.to::<u16>())
710    } else {
711        None
712    }
713}
714
715impl From<&OrderRequest> for OrderContext {
716    fn from(value: &OrderRequest) -> Self {
717        Self {
718            perpetual_id: value.perpId.to(),
719            account_id: value.accountId.to(),
720            request_id: value.orderDescId.to(),
721            order_id: request_order_id(value.orderId),
722            r#type: value.orderType.into(),
723            price: value.pricePNS,
724            expiry_block: value.expiryBlock.to(),
725            leverage: value.leverageHdths,
726            post_only: value.postOnly,
727            fill_or_kill: value.fillOrKill,
728            immediate_or_cancel: value.immediateOrCancel,
729            // V1 entrypoints cannot carry builder attribution
730            builder: None,
731            maker_fills: vec![],
732            clearing_remaining_order: false,
733            position_closed_at_log_index: None,
734        }
735    }
736}
737
738impl From<&OrderRequestV2> for OrderContext {
739    fn from(value: &OrderRequestV2) -> Self {
740        Self {
741            perpetual_id: value.perpId.to(),
742            account_id: value.accountId.to(),
743            request_id: value.orderDescId.to(),
744            order_id: request_order_id(value.orderId),
745            r#type: value.orderType.into(),
746            price: value.pricePNS,
747            expiry_block: value.expiryBlock.to(),
748            leverage: value.leverageHdths,
749            post_only: value.postOnly,
750            fill_or_kill: value.fillOrKill,
751            immediate_or_cancel: value.immediateOrCancel,
752            // Attribution is not duplicated as event fields: it is recovered by
753            // decoding the raw envelope the request carried. An envelope the
754            // contract itself rejected leaves no attribution, and the contract
755            // reports the rejection separately - either by reverting or with
756            // `OrderExtensionRejected`.
757            builder: types::BuilderAttribution::decode(&value.extension)
758                .ok()
759                .flatten(),
760            maker_fills: vec![],
761            clearing_remaining_order: false,
762            position_closed_at_log_index: None,
763        }
764    }
765}