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        #[debug("{fee}")]
254        fee: UD64, // Precision of SC calculations is limited to 5 decimals.
255        /// Portion of `fee` earned by the builder the order is attributed to,
256        /// routed entirely to the protocol balance and paid out off-chain.
257        ///
258        /// Included in `fee`, so consumers must not add it on top. Always zero
259        /// on close/decrease and liquidation fills, and on contracts without
260        /// builder attribution - a non-zero
261        /// [`OrderEvent::builder`] does not imply a non-zero fee here.
262        #[debug("{builder_fee}")]
263        builder_fee: UD64,
264        is_maker: bool,
265    },
266
267    /// Order placed to the book.
268    Placed {
269        r#type: types::OrderType,
270        #[debug("{price}")]
271        price: UD64,
272        #[debug("{size}")]
273        size: UD64,
274        expiry_block: u64,
275        #[debug("{leverage}")]
276        leverage: UD64,
277        post_only: bool,
278        fill_or_kill: bool,
279        immediate_or_cancel: bool,
280    },
281
282    /// Order removed from the book.
283    Removed,
284
285    /// Order in the book updated.
286    Updated {
287        #[debug("{:?}", price.map(|v| format!("{v}")))]
288        price: Option<UD64>,
289        #[debug("{:?}", size.map(|v| format!("{v}")))]
290        size: Option<UD64>,
291        expiry_block: Option<u64>,
292    },
293}
294
295/// Perpetual contract state or configuration mutation event.
296#[derive(Clone, derive_more::Debug)]
297pub struct PerpetualEvent {
298    /// ID of the affected perpetual contract.
299    pub perpetual_id: types::PerpetualId,
300
301    /// Type of the event with corresponding details.
302    pub r#type: PerpetualEventType,
303}
304
305/// Type of perpetual event with corresponding details.
306// See the note on `ExchangeEvent` about the `FeeSchedule` variant's size.
307#[allow(clippy::large_enum_variant)]
308#[derive(Clone, Copy, derive_more::Debug)]
309pub enum PerpetualEventType {
310    /// Perpetual contract being added
311    Added,
312
313    /// Funding event occured and rate updated.
314    FundingEvent {
315        #[debug("{rate}")]
316        rate: D64,
317        #[debug("{payment_per_unit}")]
318        payment_per_unit: D256,
319    },
320
321    /// Fee schedule of the contract updated, taking effect on its next fill.
322    ///
323    /// Emitted when the contract's own schedule is rewritten, when it is
324    /// repointed at another schedule ([`FeeSchedule::key`] changed), and when
325    /// the exchange-wide schedule it points at is rewritten.
326    FeeScheduleUpdated(FeeSchedule),
327
328    /// Funding sum scaling exponent updated. The exponent `e` defines the
329    /// divider `10^e` applied when interpreting on-chain funding sums and
330    /// per-unit funding payments for premium PnL calculations.
331    FundingSumScalingExpUpdated(u8),
332
333    /// Initial margin requirement updated.
334    InitialMarginFractionUpdated(#[debug("{_0}")] UD64),
335
336    /// Last price updated.
337    LastPriceUpdated(#[debug("{_0}")] UD64),
338
339    /// Maintenance margin requirement updated.
340    MaintenanceMarginFractionUpdated(#[debug("{_0}")] UD64),
341
342    /// Mark price updated.
343    MarkPriceUpdated(#[debug("{_0}")] UD64),
344
345    /// Base (fee tier 0) maker fee updated.
346    ///
347    /// Deprecated: only replayed from pre-v1.1.7.4 history, where fees were a
348    /// single per-contract pair. Current contracts report every fee change as
349    /// [`PerpetualEventType::FeeScheduleUpdated`].
350    MakerFeeUpdated(#[debug("{_0}")] UD64),
351
352    /// Open interest updated.
353    OpenInterestUpdated(#[debug("{_0}")] UD128),
354
355    /// Oracle configuration updated.
356    OracleConfigurationUpdated { is_used: bool, feed_id: B256 },
357
358    /// Oracle price updated.
359    OraclePriceUpdated(#[debug("{_0}")] UD64),
360
361    /// Perpetual contract paused/unpaused.
362    Paused(bool),
363
364    /// Base (fee tier 0) taker fee updated.
365    ///
366    /// Deprecated, see [`PerpetualEventType::MakerFeeUpdated`].
367    TakerFeeUpdated(#[debug("{_0}")] UD64),
368}
369
370/// Position state mutation event.
371#[derive(Clone, derive_more::Debug)]
372pub struct PositionEvent {
373    /// ID of the perpetual contract of the position.
374    pub perpetual_id: types::PerpetualId,
375
376    /// ID of the account holding the position.
377    pub account_id: types::AccountId,
378
379    /// ID of the order request resulted in this event,
380    /// if applicable.
381    pub request_id: Option<types::RequestId>,
382
383    /// Type of the event with corresponding details.
384    pub r#type: PositionEventType,
385}
386
387/// Type of position event with corresponding details.
388#[derive(Clone, Copy, derive_more::Debug)]
389pub enum PositionEventType {
390    /// Position closed.
391    Closed {
392        r#type: position::PositionType,
393        #[debug("{entry_price}")]
394        entry_price: UD64,
395        #[debug("{exit_price}")]
396        exit_price: UD64,
397        #[debug("{size}")]
398        size: UD64,
399        #[debug("{delta_pnl}")]
400        delta_pnl: D256,
401        #[debug("{premium_pnl}")]
402        premium_pnl: D256,
403    },
404
405    /// Position collateral decreased.
406    CollateralDecreased {
407        #[debug("{prev_entry_price}")]
408        prev_entry_price: UD64,
409        #[debug("{new_entry_price}")]
410        new_entry_price: UD64,
411        #[debug("{deposit}")]
412        deposit: UD128,
413    },
414
415    /// Position decreased.
416    Decreased {
417        #[debug("{prev_size}")]
418        prev_size: UD64,
419        #[debug("{new_size}")]
420        new_size: UD64,
421        #[debug("{deposit}")]
422        deposit: UD128,
423        #[debug("{delta_pnl}")]
424        delta_pnl: D256,
425        #[debug("{premium_pnl}")]
426        premium_pnl: D256,
427    },
428
429    /// Position deleveraged.
430    Deleveraged {
431        force_close: bool,
432        r#type: position::PositionType,
433        #[debug("{entry_price}")]
434        entry_price: UD64,
435        #[debug("{exit_price}")]
436        exit_price: UD64,
437        #[debug("{prev_size}")]
438        prev_size: UD64,
439        #[debug("{new_size}")]
440        new_size: UD64,
441        #[debug("{deposit}")]
442        deposit: UD128,
443        #[debug("{delta_pnl}")]
444        delta_pnl: D256,
445        #[debug("{premium_pnl}")]
446        premium_pnl: D256,
447    },
448
449    /// Position deposit(collateral) updated.
450    DepositUpdated(#[debug("{_0}")] UD128),
451
452    /// Position increased.
453    Increased {
454        #[debug("{entry_price}")]
455        entry_price: UD64,
456        #[debug("{prev_size}")]
457        prev_size: UD64,
458        #[debug("{new_size}")]
459        new_size: UD64,
460        #[debug("{deposit}")]
461        deposit: UD128,
462    },
463
464    /// Position inverted.
465    Inverted {
466        r#type: position::PositionType,
467        #[debug("{entry_price}")]
468        entry_price: UD64,
469        #[debug("{prev_size}")]
470        prev_size: UD64,
471        #[debug("{new_size}")]
472        new_size: UD64,
473        #[debug("{deposit}")]
474        deposit: UD128,
475        #[debug("{delta_pnl}")]
476        delta_pnl: D256,
477        #[debug("{premium_pnl}")]
478        premium_pnl: D256,
479    },
480
481    /// Position liquidated.
482    Liquidated {
483        r#type: position::PositionType,
484        #[debug("{entry_price}")]
485        entry_price: UD64,
486        #[debug("{exit_price}")]
487        exit_price: UD64,
488        #[debug("{prev_size}")]
489        prev_size: UD64,
490        #[debug("{liquidated_size}")]
491        liquidated_size: UD64,
492        #[debug("{new_size}")]
493        new_size: UD64,
494        #[debug("{deposit}")]
495        deposit: UD128,
496        #[debug("{delta_pnl}")]
497        delta_pnl: D256,
498        #[debug("{premium_pnl}")]
499        premium_pnl: D256,
500    },
501
502    /// Position maintenance margin requirement updated due
503    /// to updated maintenane margin fraction.
504    MaintenanceMarginUpdated(#[debug("{_0}")] UD128),
505
506    /// Position opened.
507    Opened {
508        r#type: position::PositionType,
509        #[debug("{entry_price}")]
510        entry_price: UD64,
511        #[debug("{size}")]
512        size: UD64,
513        #[debug("{deposit}")]
514        deposit: UD128,
515    },
516
517    /// Position unrealized PnL updated.
518    UnrealizedPnLUpdated {
519        #[debug("{pnl}")]
520        pnl: D256,
521        #[debug("{delta_pnl}")]
522        delta_pnl: D256,
523        #[debug("{premium_pnl}")]
524        premium_pnl: D256,
525    },
526
527    /// Position unwound.
528    Unwound {
529        r#type: position::PositionType,
530        #[debug("{entry_price}")]
531        entry_price: UD64,
532        #[debug("{exit_price}")]
533        exit_price: UD64,
534        #[debug("{size}")]
535        size: UD64,
536        #[debug("{fair_market_value}")]
537        fair_market_value: D256,
538        #[debug("{payment}")]
539        payment: UD128,
540    },
541}
542
543impl StateEvents {
544    pub fn as_account_event(&self) -> Option<AccountEvent> {
545        if let StateEvents::Account(account_event) = self {
546            Some(account_event.clone())
547        } else {
548            None
549        }
550    }
551
552    pub fn as_error(&self) -> Option<OrderError> {
553        if let StateEvents::Error(error_event) = self { Some(error_event.clone()) } else { None }
554    }
555
556    pub fn as_order_event(&self) -> Option<OrderEvent> {
557        if let StateEvents::Order(order_event) = self { Some(order_event.clone()) } else { None }
558    }
559
560    pub fn as_exchange_event(&self) -> Option<ExchangeEvent> {
561        if let StateEvents::Exchange(exchange_event) = self {
562            Some(exchange_event.clone())
563        } else {
564            None
565        }
566    }
567
568    pub fn as_perpetual_event(&self) -> Option<PerpetualEvent> {
569        if let StateEvents::Perpetual(perpetual_event) = self {
570            Some(perpetual_event.clone())
571        } else {
572            None
573        }
574    }
575
576    pub fn as_position_event(&self) -> Option<PositionEvent> {
577        if let StateEvents::Position(position_event) = self {
578            Some(position_event.clone())
579        } else {
580            None
581        }
582    }
583
584    pub fn as_trade(&self) -> Option<types::Trade> {
585        if let StateEvents::Trade(trade) = self { Some(trade.clone()) } else { None }
586    }
587
588    pub(crate) fn account(
589        acc: &account::Account,
590        ctx: &Option<OrderContext>,
591        r#type: AccountEventType,
592    ) -> Self {
593        Self::Account(AccountEvent {
594            account_id: acc.id(),
595            request_id: ctx.as_ref().map(|c| c.request_id),
596            r#type,
597        })
598    }
599
600    pub(crate) fn order(
601        perp: &perpetual::Perpetual,
602        ord: &order::Order,
603        ctx: &Option<OrderContext>,
604        r#type: OrderEventType,
605    ) -> Self {
606        Self::Order(OrderEvent {
607            perpetual_id: perp.id(),
608            account_id: ord.account_id(),
609            request_id: ctx.as_ref().map(|c| c.request_id),
610            client_order_id: ord.client_order_id(),
611            order_id: Some(ord.order_id()),
612            builder: ord.builder(),
613            r#type,
614        })
615    }
616
617    pub(crate) fn order_error(ctx: &OrderContext, r#type: OrderErrorType) -> StateEvents {
618        Self::Error(OrderError {
619            perpetual_id: ctx.perpetual_id,
620            account_id: ctx.account_id,
621            request_id: ctx.request_id,
622            order_id: ctx.order_id,
623            r#type,
624        })
625    }
626
627    pub(crate) fn affected_order_error(
628        ctx: &OrderContext,
629        ord: &order::Order,
630        r#type: OrderErrorType,
631    ) -> StateEvents {
632        Self::Error(OrderError {
633            perpetual_id: ctx.perpetual_id,
634            account_id: ord.account_id(),
635            request_id: ctx.request_id,
636            order_id: Some(ord.order_id()),
637            r#type,
638        })
639    }
640
641    pub(crate) fn perpetual(
642        perp: &perpetual::Perpetual,
643        r#type: PerpetualEventType,
644    ) -> StateEvents {
645        Self::Perpetual(PerpetualEvent { perpetual_id: perp.id(), r#type })
646    }
647
648    pub(crate) fn position(
649        pos: &position::Position,
650        ctx: &Option<OrderContext>,
651        r#type: PositionEventType,
652    ) -> Self {
653        Self::Position(PositionEvent {
654            perpetual_id: pos.perpetual_id(),
655            account_id: pos.account_id(),
656            request_id: ctx.as_ref().map(|c| c.request_id),
657            r#type,
658        })
659    }
660
661    pub(crate) fn trade(ctx: &OrderContext, taker_fee: UD64, taker_builder_fee: UD64) -> Self {
662        Self::Trade(types::Trade {
663            perpetual_id: ctx.perpetual_id,
664            taker_account_id: ctx.account_id,
665            taker_request_id: ctx.request_id,
666            taker_side: ctx.r#type.try_side().expect("order type with side"),
667            taker_fee,
668            taker_builder: ctx.builder,
669            taker_builder_fee,
670            maker_fills: ctx.maker_fills.clone(),
671        })
672    }
673}
674
675/// Order request context.
676#[derive(Debug)]
677pub(crate) struct OrderContext {
678    pub(crate) perpetual_id: types::PerpetualId,
679    pub(crate) account_id: types::AccountId,
680    pub(crate) request_id: types::RequestId,
681    pub(crate) order_id: Option<types::OrderId>,
682    pub(crate) r#type: types::RequestType,
683    pub(crate) price: U256,
684    pub(crate) expiry_block: u64,
685    pub(crate) leverage: U256,
686    pub(crate) post_only: bool,
687    pub(crate) fill_or_kill: bool,
688    pub(crate) immediate_or_cancel: bool,
689    pub(crate) builder: Option<types::BuilderAttribution>,
690    pub(crate) maker_fills: Vec<types::MakerFill>,
691    pub(crate) clearing_remaining_order: bool,
692    pub(crate) position_closed_at_log_index: Option<u64>,
693}
694
695/// Order ID of a request, `None` for trigger order requests - their order IDs
696/// might exceed `u16::MAX` and they are not supported by the SDK yet.
697fn request_order_id(order_id: U256) -> Option<types::OrderId> {
698    if order_id <= U256::from(u16::MAX) {
699        std::num::NonZeroU16::new(order_id.to::<u16>())
700    } else {
701        None
702    }
703}
704
705impl From<&OrderRequest> for OrderContext {
706    fn from(value: &OrderRequest) -> Self {
707        Self {
708            perpetual_id: value.perpId.to(),
709            account_id: value.accountId.to(),
710            request_id: value.orderDescId.to(),
711            order_id: request_order_id(value.orderId),
712            r#type: value.orderType.into(),
713            price: value.pricePNS,
714            expiry_block: value.expiryBlock.to(),
715            leverage: value.leverageHdths,
716            post_only: value.postOnly,
717            fill_or_kill: value.fillOrKill,
718            immediate_or_cancel: value.immediateOrCancel,
719            // V1 entrypoints cannot carry builder attribution
720            builder: None,
721            maker_fills: vec![],
722            clearing_remaining_order: false,
723            position_closed_at_log_index: None,
724        }
725    }
726}
727
728impl From<&OrderRequestV2> for OrderContext {
729    fn from(value: &OrderRequestV2) -> Self {
730        Self {
731            perpetual_id: value.perpId.to(),
732            account_id: value.accountId.to(),
733            request_id: value.orderDescId.to(),
734            order_id: request_order_id(value.orderId),
735            r#type: value.orderType.into(),
736            price: value.pricePNS,
737            expiry_block: value.expiryBlock.to(),
738            leverage: value.leverageHdths,
739            post_only: value.postOnly,
740            fill_or_kill: value.fillOrKill,
741            immediate_or_cancel: value.immediateOrCancel,
742            // Attribution is not duplicated as event fields: it is recovered by
743            // decoding the raw envelope the request carried. An envelope the
744            // contract itself rejected leaves no attribution, and the contract
745            // reports the rejection separately - either by reverting or with
746            // `OrderExtensionRejected`.
747            builder: types::BuilderAttribution::decode(&value.extension)
748                .ok()
749                .flatten(),
750            maker_fills: vec![],
751            clearing_remaining_order: false,
752            position_closed_at_log_index: None,
753        }
754    }
755}