Skip to main content

perpl_sdk/state/
exchange.rs

1use std::iter;
2
3use fastnum::{D64, D256, UD64, UD128};
4use itertools::chain;
5
6use super::*;
7use crate::{
8    Chain,
9    abi::dex::Exchange::ExchangeEvents,
10    stream,
11    types::{EventContext, OrderType},
12};
13
14pub type StateBlockEvents = types::BlockEvents<types::EventContext<Vec<StateEvents>>>;
15
16/// Raw maker fill data, common to the V1 and V2 `MakerOrderFilled*` events.
17struct RawMakerFill {
18    perp_id: U256,
19    account_id: U256,
20    order_id: U256,
21    price_pns: U256,
22    lot_lns: U256,
23    fee_cns: U256,
24    builder_fee_cns: U256,
25    locked_balance_cns: U256,
26    balance_cns: U256,
27}
28
29/// Raw listing data, common to the V1 and V2 `ContractAdded*` events. The
30/// versions differ in how the new contract's fees are reported: V1 carried the
31/// resolved base rates, V2 carries the fee schedule key they resolve from.
32struct RawContractAdded {
33    perp_id: U256,
34    status: u8,
35    price_decimals: U256,
36    lot_decimals: U256,
37    base_price_pns: U256,
38    init_margin_frac_hdths: U256,
39    maint_margin_frac_hdths: U256,
40}
41
42/// Raw taker fill data, common to the V1 and V2 `TakerOrderFilled*` events.
43struct RawTakerFill {
44    collat_price_pns: U256,
45    lot_lns: U256,
46    fee_cns: U256,
47    builder_fee_cns: U256,
48    balance_cns: U256,
49}
50
51/// Exchange state snapshot.
52///
53/// [`super::SnapshotBuilder`] can be used to create the snapshot at
54/// specified/latest block, which can then be kept up to date by
55/// calling [`Self::apply_events`] with events from [`crate::stream::raw`].
56#[derive(Clone, derive_more::Debug)]
57pub struct Exchange {
58    chain: Chain,
59    instant: types::StateInstant,
60    features: ContractFeatures,
61    collateral_converter: num::Converter,
62    funding_interval_blocks: u32,
63    #[debug("{min_post}")]
64    min_post: UD128,
65    #[debug("{min_settle}")]
66    min_settle: UD128,
67    #[debug("{recycle_fee}")]
68    recycle_fee: UD128,
69    fee_schedules: FeeScheduleRegistry,
70    perpetuals: HashMap<types::PerpetualId, Perpetual>,
71    accounts: HashMap<types::AccountId, Account>,
72    is_halted: bool,
73    track_all_accounts: bool,
74}
75
76impl Exchange {
77    #[allow(clippy::too_many_arguments)]
78    pub(crate) fn new(
79        chain: Chain,
80        instant: types::StateInstant,
81        features: ContractFeatures,
82        collateral_converter: num::Converter,
83        funding_interval_blocks: u32,
84        min_post: UD128,
85        min_settle: UD128,
86        recycle_fee: UD128,
87        fee_schedules: FeeScheduleRegistry,
88        perpetuals: HashMap<types::PerpetualId, Perpetual>,
89        accounts: HashMap<types::AccountId, Account>,
90        is_halted: bool,
91        track_all_accounts: bool,
92    ) -> Self {
93        Self {
94            chain,
95            instant,
96            features,
97            collateral_converter,
98            funding_interval_blocks,
99            min_post,
100            min_settle,
101            recycle_fee,
102            fee_schedules,
103            perpetuals,
104            accounts,
105            is_halted,
106            track_all_accounts,
107        }
108    }
109
110    /// Revision of the exchange smart contract the SDK targeted at.
111    pub const fn revision() -> &'static str { crate::abi::DEX_REVISION }
112
113    /// Chain the snapshot collected from.
114    pub fn chain(&self) -> &Chain { &self.chain }
115
116    /// Instant the snapshot is consistent with or was last updated at.
117    pub fn instant(&self) -> types::StateInstant { self.instant }
118
119    /// Feature set of the *deployed* contract, which can lag behind the
120    /// revision the SDK targets ([`Self::revision`]). Detected while
121    /// building the snapshot and kept up to date by `ContractVersionSet`.
122    pub fn features(&self) -> ContractFeatures { self.features }
123
124    /// Version reported by the deployed contract, `None` before v1.1.7.4 which
125    /// has no version getter.
126    pub fn contract_version(&self) -> Option<ContractVersion> { self.features.version() }
127
128    /// Every fee schedule known to the exchange, which perpetual contracts
129    /// resolve their fees from by pointing at one (see
130    /// [`Perpetual::fee_schedule`]).
131    pub fn fee_schedules(&self) -> &FeeScheduleRegistry { &self.fee_schedules }
132
133    /// Exchange-wide default fee schedule, shared by every perpetual contract
134    /// that has not been repointed at another one.
135    pub fn default_fee_schedule(&self) -> FeeSchedule { self.fee_schedules.default_schedule() }
136
137    /// Exchange-wide default fee schedule for real-world assets.
138    pub fn rwa_fee_schedule(&self) -> FeeSchedule { self.fee_schedules.rwa_default_schedule() }
139
140    /// Fee schedule registered under the given key, `None` for a custom
141    /// schedule that has never been observed - one keyed by a perpetual the
142    /// snapshot does not track, and not written since.
143    pub fn fee_schedule(&self, key: FeeScheduleKey) -> Option<FeeSchedule> {
144        self.fee_schedules.get(key)
145    }
146
147    /// Converter of fixed-point <-> decimal numbers for collateral token
148    /// amounts.
149    pub fn collateral_converter(&self) -> num::Converter { self.collateral_converter }
150
151    /// Funding interval in blocks.
152    ///
153    /// Each perpetual contract has own [Perpetual::funding_start_block]  this
154    /// interval applied to.
155    pub fn funding_interval_blocks(&self) -> u32 { self.funding_interval_blocks }
156
157    /// Minimal amount in collateral token that can be posted to the book.
158    pub fn min_post(&self) -> UD128 { self.min_post }
159
160    /// Minimal amount in collateral token that can be settled.
161    pub fn min_settle(&self) -> UD128 { self.min_settle }
162
163    /// Amount in collateral token locked with each posted order to
164    /// pay the account that cleans it up:
165    /// * When cancelled/changed by the original poster -> the original poster
166    /// * When filled -> the original poster
167    /// * In all other cases -> the one that performed the recycling
168    pub fn recycle_fee(&self) -> UD128 { self.recycle_fee }
169
170    /// Perpetual contracts state tracked within the exchange, according to
171    /// initial snapshot building configuration.
172    pub fn perpetuals(&self) -> &HashMap<types::PerpetualId, Perpetual> { &self.perpetuals }
173
174    /// Accounts state tracked within the exchange, according to initial
175    /// snapshot building configuration.
176    pub fn accounts(&self) -> &HashMap<types::AccountId, Account> { &self.accounts }
177
178    /// Indicates if exchange is being halted.
179    pub fn is_halted(&self) -> bool { self.is_halted }
180
181    /// Updates state snapshot by applying raw exchange events from the
182    /// specific block.
183    ///
184    /// Blocks expected to arrive strictly in-order, with already applied blocks
185    /// being ignored, to enforce state consistency as most raw events
186    /// provide only incremental state update information rather than full
187    /// piece of state snapshot.
188    ///
189    /// Exchange emits two categories of events:
190    /// * State mutation events
191    /// * Order request error responses, for requests issued in batches via
192    ///   [`crate::abi::dex::Exchange::ExchangeInstance::execOrders`] with
193    ///   `revertOnFail` = false.
194    ///
195    /// This method applies state mutation events only to tracked perpetual
196    /// contracts and accounts provided to [`SnapshotBuilder`] during the
197    /// initial snapshot creation, and returns order request failure events
198    /// only for requests issues by tracked accounts. Successfull order book
199    /// mutations are applied to all orders of tracked perpetual contracts,
200    /// so client code can keep up to date order book representation externally
201    /// if needed.
202    ///
203    /// # Returns
204    ///
205    /// On success, list of state mutation and failure [`StateEvents`] produced
206    /// from the original raw events, filtered as described above and with
207    /// numeric systems conversion applied.
208    ///
209    /// [`StateEvents`] are roughly resemble
210    /// [`crate::abi::dex::Exchange::ExchangeEvents`] so corresponding smart
211    /// contract documentation and raw event data for error responses could be
212    /// helpful with debugging, but there is no exact match and more than
213    /// one state event can be emitted in response to a single raw event, eg.
214    /// processing of single order event produces up to two account events on
215    /// top of order events within the same event context.
216    ///
217    /// On failure, the corresponding [`DexError`], any of which indicates some
218    /// inconsistency in event sequence or event handling logic and should
219    /// not be ignored as it may lead to state inconsistency.
220    pub fn apply_events(
221        &mut self,
222        events: &stream::RawBlockEvents,
223    ) -> Result<Option<StateBlockEvents>, DexError> {
224        let next_instant = events.instant();
225        if self.instant >= next_instant {
226            // Block already applied
227            return Ok(None);
228        }
229        if self.instant.block_number() + 1 < next_instant.block_number() {
230            // Block arrived out of order
231            return Err(DexError::BlockOutOfOrder(
232                self.instant.block_number() + 1,
233                next_instant.block_number(),
234            ));
235        }
236
237        // apply_events runs three passes over the block:
238        // Pass 1 — funding: settle the block's scheduled funding on each
239        // position's pre-event size (before any decreases).
240        // Pass 2 — raw events: apply the block's on-chain events
241        // in order (orders, position changes, perpetual-parameter updates);
242        // a perpetual-parameter change updates the perpetual here and is set
243        // aside for Pass 3.
244        // Pass 3 — fan-out: fan the perpetual-parameter changes set aside in
245        // Pass 2 (e.g. a maintenance-margin-fraction change) out to
246        // every tracked position.
247        let mut order_context: Option<OrderContext> = None;
248        let mut prev_tx_index: Option<u64> = None;
249        let mut state_events = vec![];
250        let mut perp_events = vec![];
251
252        // Pass 1 — funding: the contract settles a funding-event block at the new
253        // funding sum regardless of same-block decreases, so funding must land
254        // on each position's PRE-event size, before the block's size-changing
255        // events. This is the only place funding is applied.
256        let funding_due: Vec<(types::PerpetualId, D64, D256)> = self
257            .perpetuals
258            .values_mut()
259            .filter_map(|perp| {
260                perp.take_funding_payment(next_instant)
261                    .map(|(rate, payment)| (perp.id(), rate, payment))
262            })
263            .collect();
264        for (perp_id, rate, payment) in funding_due {
265            let mut funding_events = vec![];
266            if let Some(perp) = self.perpetuals.get(&perp_id) {
267                funding_events.push(StateEvents::perpetual(
268                    perp,
269                    PerpetualEventType::FundingEvent { rate, payment_per_unit: payment },
270                ));
271            }
272            for acc in self.accounts.values_mut() {
273                if let Some(pos) = acc.positions_mut().get_mut(&perp_id)
274                    && pos.apply_funding_payment(next_instant, payment)
275                {
276                    funding_events.push(StateEvents::position(
277                        pos,
278                        &None,
279                        PositionEventType::UnrealizedPnLUpdated {
280                            pnl: pos.pnl(),
281                            delta_pnl: pos.delta_pnl(),
282                            premium_pnl: pos.premium_pnl(),
283                        },
284                    ));
285                }
286            }
287            if !funding_events.is_empty() {
288                state_events.push(EventContext::empty(funding_events));
289            }
290        }
291
292        // Pass 2 — raw events: apply the block's on-chain events in order, keeping
293        // incremental order context across events within a transaction.
294        for event in events.events() {
295            if prev_tx_index.is_some_and(|idx| idx < event.tx_index()) {
296                // Reset order context at the transaction boundary
297                order_context.take();
298            }
299            let result = self.apply_raw_event(next_instant, event, &mut order_context)?;
300            if !result.is_empty() {
301                // Set aside perpetual-parameter events for the Pass 3 fan-out below.
302                let block_perp_events = result
303                    .iter()
304                    .filter(|e| e.as_perpetual_event().is_some())
305                    .cloned()
306                    .collect::<Vec<_>>();
307                if !block_perp_events.is_empty() {
308                    perp_events.push(block_perp_events);
309                }
310                state_events.push(event.pass(result));
311            }
312            prev_tx_index = Some(event.tx_index());
313        }
314
315        // Commit the instant: advance each perpetual's state instant and expire stale
316        // orders.
317        self.instant = events.instant();
318        for perp in self.perpetuals.values_mut() {
319            perp.update_state_instant(self.instant);
320        }
321
322        // Pass 3 — fan-out: apply the perpetual-parameter changes set aside in Pass 2
323        // (e.g. a maintenance-margin-fraction change) to every tracked
324        // position.
325        for event in perp_events.iter().flatten() {
326            let result = self.apply_state_event(self.instant, event)?;
327            if !result.is_empty() {
328                state_events.push(EventContext::empty(result));
329            }
330        }
331
332        Ok(Some(StateBlockEvents::new(self.instant, state_events)))
333    }
334
335    pub(crate) fn apply_raw_event(
336        &mut self,
337        instant: types::StateInstant,
338        event: &stream::RawEvent,
339        ctx: &mut Option<OrderContext>,
340    ) -> Result<Vec<StateEvents>, DexError> {
341        let cc = self.collateral_converter;
342
343        let must_ctx = || {
344            ctx.as_ref()
345                .ok_or(DexError::OrderContextExpected(event.tx_index(), event.log_index()))
346        };
347
348        Ok(match event.event() {
349            ExchangeEvents::AccountCreated(e) => {
350                if self.track_all_accounts {
351                    self.accounts
352                        .insert(e.id.to(), Account::created(instant, e.id.to(), e.account));
353                    vec![StateEvents::Account(AccountEvent {
354                        account_id: e.id.to(),
355                        request_id: None,
356                        r#type: AccountEventType::Created(e.id.to()),
357                    })]
358                } else {
359                    vec![]
360                }
361            },
362            ExchangeEvents::AccountFeeTierSet(e) => self
363                .account(e.accountId)
364                .map(|acc| {
365                    let tier = e.tier.to();
366                    acc.update_fee_tier(instant, tier);
367                    StateEvents::account(acc, ctx, AccountEventType::FeeTierUpdated(tier))
368                })
369                .into_iter()
370                .collect(),
371            ExchangeEvents::AccountFreeze(e) => self
372                .account(e.accountId)
373                .map(|acc| {
374                    acc.update_frozen(instant, e.status > 0);
375                    StateEvents::account(acc, ctx, AccountEventType::Frozen(acc.frozen()))
376                })
377                .into_iter()
378                .collect(),
379            ExchangeEvents::AccountFrozen(_) => self
380                .err_ctx(ctx, event)?
381                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::AccountFrozen))
382                .into_iter()
383                .collect(),
384            ExchangeEvents::AccountLiquidationCredit(e) => self
385                .account(e.accountId)
386                .map(|acc| {
387                    acc.update_balance(instant, cc.from_unsigned(e.endBalanceCNS));
388                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
389                })
390                .into_iter()
391                .collect(),
392            ExchangeEvents::AdminChanged(_) => vec![],
393            ExchangeEvents::AdministratorUpdated(_) => vec![],
394            ExchangeEvents::AmountExceedsAvailableBalance(e) => self
395                .err_ctx(ctx, event)?
396                .map(|ctx| {
397                    StateEvents::order_error(
398                        ctx,
399                        OrderErrorType::AmountExceedsAvailableBalance(
400                            cc.from_unsigned(e.amountCNS),
401                            cc.from_unsigned(e.availableBalanceCNS),
402                        ),
403                    )
404                })
405                .into_iter()
406                .collect(),
407            ExchangeEvents::BankruptcyPricePreventsDeleverage(_) => vec![],
408            ExchangeEvents::BeaconUpgraded(_) => vec![],
409            ExchangeEvents::BlockStatusChanged(_) => vec![],
410            ExchangeEvents::BorrowMarginNotMetAfterDecCollateral(_) => vec![],
411            ExchangeEvents::BuyToLiquidateSettled(_) => vec![],
412            ExchangeEvents::BuyToLiquidateSlippageExceeded(_) => vec![],
413            ExchangeEvents::BuyToLiquidateStarted(_) => vec![],
414            ExchangeEvents::BuyToLiquidateBuyerRestricted(_) => vec![],
415            ExchangeEvents::BuyToLiquidateParamsUpdated(_) => vec![],
416            ExchangeEvents::BuyToLiquidateThresholdUpdated(_) => vec![],
417            ExchangeEvents::BuyToLiquidateRestrictionUpdated(_) => vec![],
418            ExchangeEvents::CancelExistingInvalidCloseOrders(_) => self
419                .err_ctx(ctx, event)?
420                .map(|ctx| {
421                    StateEvents::order_error(ctx, OrderErrorType::CancelExistingInvalidCloseOrders)
422                })
423                .into_iter()
424                .collect(),
425            ExchangeEvents::CannotAdjustEntryPriceToDecCollateral(_) => vec![],
426            ExchangeEvents::CantBuyToLiquidate(_) => vec![],
427            ExchangeEvents::CantChangeCloseOrder(_) => self
428                .err_ctx(ctx, event)?
429                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::CantChangeCloseOrder))
430                .into_iter()
431                .collect(),
432            ExchangeEvents::CantDeleverageAgainstOpposingPositions(_) => vec![],
433            ExchangeEvents::CantLiquidatePosAboveMMR(_) => vec![],
434            ExchangeEvents::ChangeExpiredOrderNeedsNewExpiry(_) => self
435                .err_ctx(ctx, event)?
436                .map(|ctx| {
437                    StateEvents::order_error(ctx, OrderErrorType::ChangeExpiredOrderNeedsNewExpiry)
438                })
439                .into_iter()
440                .collect(),
441            ExchangeEvents::ClearingExpiredOrder(e) => chain!(
442                if let Some(perp) = self.perpetual(e.perpId) {
443                    let order_id = std::num::NonZeroU16::new(e.orderId.to::<u16>())
444                        .expect("orderId in event cannot be 0");
445                    let order = perp.remove_order(order_id)?;
446                    Some(StateEvents::order(perp, &order, ctx, OrderEventType::Removed))
447                } else {
448                    None
449                },
450                self.account(e.accountId).map(|acc| {
451                    acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
452                    StateEvents::account(
453                        acc,
454                        ctx,
455                        AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
456                    )
457                }),
458                if !e.recyclerAmountCNS.is_zero() {
459                    self.account(e.recyclerAccountId).map(|acc| {
460                        acc.update_balance(instant, cc.from_unsigned(e.recyclerBalanceCNS));
461                        StateEvents::account(
462                            acc,
463                            ctx,
464                            AccountEventType::BalanceUpdated(acc.balance()),
465                        )
466                    })
467                } else {
468                    None
469                },
470            )
471            .collect(),
472            ExchangeEvents::ClearingFrozenAccountOrder(e) => chain!(
473                if let Some(perp) = self.perpetual(e.perpId) {
474                    let order_id = std::num::NonZeroU16::new(e.orderId.to::<u16>())
475                        .expect("orderId in event cannot be 0");
476                    let order = perp.remove_order(order_id)?;
477                    Some(StateEvents::order(perp, &order, ctx, OrderEventType::Removed))
478                } else {
479                    None
480                },
481                self.account(e.accountId).map(|acc| {
482                    acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
483                    StateEvents::account(
484                        acc,
485                        ctx,
486                        AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
487                    )
488                }),
489                if !e.recyclerAmountCNS.is_zero() {
490                    self.account(e.recyclerAccountId).map(|acc| {
491                        acc.update_balance(instant, cc.from_unsigned(e.recyclerBalanceCNS));
492                        StateEvents::account(
493                            acc,
494                            ctx,
495                            AccountEventType::BalanceUpdated(acc.balance()),
496                        )
497                    })
498                } else {
499                    None
500                },
501            )
502            .collect(),
503            ExchangeEvents::ClearingInvalidCloseOrder(e) => chain!(
504                if let Some(perp) = self.perpetual(e.perpId) {
505                    let order_id = std::num::NonZeroU16::new(e.orderId.to::<u16>())
506                        .expect("orderId in event cannot be 0");
507                    let order = perp.remove_order(order_id)?;
508                    Some(StateEvents::order(perp, &order, ctx, OrderEventType::Removed))
509                } else {
510                    None
511                },
512                self.account(e.accountId).map(|acc| {
513                    acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
514                    StateEvents::account(
515                        acc,
516                        ctx,
517                        AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
518                    )
519                }),
520                if !e.recyclerAmountCNS.is_zero() {
521                    self.account(e.recyclerAccountId).map(|acc| {
522                        acc.update_balance(instant, cc.from_unsigned(e.recyclerBalanceCNS));
523                        StateEvents::account(
524                            acc,
525                            ctx,
526                            AccountEventType::BalanceUpdated(acc.balance()),
527                        )
528                    })
529                } else {
530                    None
531                },
532            )
533            .collect(),
534            ExchangeEvents::ClearingRemainingOrderLockBeyondBalance(e) => {
535                if let Some(ctx) = ctx {
536                    ctx.clearing_remaining_order = true;
537                }
538                chain!(if !e.recyclerAmountCNS.is_zero() {
539                    self.account(e.recyclerAccountId).map(|acc| {
540                        acc.update_balance(instant, cc.from_unsigned(e.recyclerBalanceCNS));
541                        StateEvents::account(
542                            acc,
543                            ctx,
544                            AccountEventType::BalanceUpdated(acc.balance()),
545                        )
546                    })
547                } else {
548                    None
549                },)
550                .collect()
551            },
552            ExchangeEvents::ClearingSelfMatchingOrder(e) => chain!(
553                if let Some(perp) = self.perpetual(e.perpId) {
554                    let order_id = std::num::NonZeroU16::new(e.orderId.to::<u16>())
555                        .expect("orderId in event cannot be 0");
556                    let order = perp.remove_order(order_id)?;
557                    Some(StateEvents::order(perp, &order, ctx, OrderEventType::Removed))
558                } else {
559                    None
560                },
561                self.account(e.accountId).map(|acc| {
562                    acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
563                    StateEvents::account(
564                        acc,
565                        ctx,
566                        AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
567                    )
568                }),
569                if !e.recyclerAmountCNS.is_zero() {
570                    self.account(e.recyclerAccountId).map(|acc| {
571                        acc.update_balance(instant, cc.from_unsigned(e.recyclerBalanceCNS));
572                        StateEvents::account(
573                            acc,
574                            ctx,
575                            AccountEventType::BalanceUpdated(acc.balance()),
576                        )
577                    })
578                } else {
579                    None
580                },
581            )
582            .collect(),
583            ExchangeEvents::CloseOrderExceedsPosition(_) => self
584                .err_ctx(ctx, event)?
585                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::CloseOrderExceedsPosition))
586                .into_iter()
587                .collect(),
588            ExchangeEvents::CloseOrderPositionMismatch(_) => self
589                .err_ctx(ctx, event)?
590                .map(|ctx| {
591                    StateEvents::order_error(ctx, OrderErrorType::CloseOrderPositionMismatch)
592                })
593                .into_iter()
594                .collect(),
595            ExchangeEvents::CollateralDecreaseDeclined(_) => vec![],
596            ExchangeEvents::CollateralDecreaseRequestCancelled(_) => vec![],
597            ExchangeEvents::CollateralDecreaseRequested(_) => vec![],
598            ExchangeEvents::CollateralDecreaseRequestExpired(_) => vec![],
599            ExchangeEvents::CollateralDeposit(e) => self
600                .account(e.accountId)
601                .map(|acc| {
602                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
603                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
604                })
605                .into_iter()
606                .collect(),
607            ExchangeEvents::CollateralWithdrawal(e) => self
608                .account(e.accountId)
609                .map(|acc| {
610                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
611                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
612                })
613                .into_iter()
614                .collect(),
615            // Superseded by `ContractAddedV2` in v1.1.7.4, replayed from earlier
616            // history only, where the listing carried resolved base fees rather
617            // than a fee schedule key
618            ExchangeEvents::ContractAdded(e) => {
619                // Per100K unconditionally: this event was retired in v1.1.7.4, so it
620                // can only be replayed from history that predates the redenomination.
621                let fee_converter = num::fee_converter();
622                vec![self.add_perpetual(
623                    instant,
624                    &e.name,
625                    &e.symbol,
626                    RawContractAdded {
627                        perp_id: e.perpId,
628                        status: e.status,
629                        price_decimals: e.priceDecimals,
630                        lot_decimals: e.lotDecimals,
631                        base_price_pns: e.basePricePNS,
632                        init_margin_frac_hdths: e.initMarginFracHdths,
633                        maint_margin_frac_hdths: e.maintMarginFracHdths,
634                    },
635                    FeeSchedule::flat(
636                        FeeScheduleKey::Default,
637                        fee_converter.from_unsigned(e.takerFeePer100K),
638                        fee_converter.from_unsigned(e.makerFeePer100K),
639                    ),
640                )]
641            },
642            ExchangeEvents::ContractAddedV2(e) => {
643                // The listing reports the contract's fee schedule KEY, the rates
644                // being resolvable from it - a new contract is placed on the
645                // exchange-wide default schedule, and no `PerpFeeSchedIdSet`
646                // accompanies the listing to report the id separately.
647                let key = FeeScheduleKey::from_raw(e.perpFeeSchedId);
648                let fee_schedule = self.fee_schedule(key).ok_or(
649                    // A custom schedule of a contract that does not exist yet has
650                    // no rates to resolve; the contract documents that a listing
651                    // is always placed on a shared schedule, so this is
652                    // unreachable in practice
653                    DexError::FeeScheduleNotFound(key),
654                )?;
655                vec![self.add_perpetual(
656                    instant,
657                    &e.name,
658                    &e.symbol,
659                    RawContractAdded {
660                        perp_id: e.perpId,
661                        status: e.status,
662                        price_decimals: e.priceDecimals,
663                        lot_decimals: e.lotDecimals,
664                        base_price_pns: e.basePricePNS,
665                        init_margin_frac_hdths: e.initMarginFracHdths,
666                        maint_margin_frac_hdths: e.maintMarginFracHdths,
667                    },
668                    fee_schedule,
669                )]
670            },
671            ExchangeEvents::ContractNotOperational(_) => self
672                .err_ctx(ctx, event)?
673                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::ContractNotOperational))
674                .into_iter()
675                .collect(),
676            ExchangeEvents::ContractLinkFeedUpdated(e) => self
677                .perpetual(e.perpId)
678                .map(|perp| {
679                    perp.update_oracle_feed_id(instant, e.feedId);
680                    StateEvents::perpetual(
681                        perp,
682                        PerpetualEventType::OracleConfigurationUpdated {
683                            is_used: perp.is_oracle_used(),
684                            feed_id: perp.oracle_feed_id(),
685                        },
686                    )
687                })
688                .into_iter()
689                .collect(),
690            ExchangeEvents::ContractPaused(e) => {
691                // Bound before the `&mut self` borrow below; a first unpause
692                // anchors the contract's funding schedule to this grid.
693                let funding_interval = self.funding_interval_blocks as u64;
694                self.perpetual(e.perpId)
695                    .map(|perp| {
696                        perp.update_paused(instant, e.paused, funding_interval);
697                        StateEvents::perpetual(perp, PerpetualEventType::Paused(perp.is_paused()))
698                    })
699                    .into_iter()
700                    .collect()
701            },
702            ExchangeEvents::ContractRemoved(e) => {
703                let funding_interval = self.funding_interval_blocks as u64;
704                self.perpetual(e.perpId)
705                    .map(|perp| {
706                        perp.update_paused(instant, true, funding_interval);
707                        StateEvents::perpetual(perp, PerpetualEventType::Paused(perp.is_paused()))
708                    })
709                    .into_iter()
710                    .collect()
711            },
712            ExchangeEvents::ContractVersionSet(e) => {
713                let version = ContractVersion::new(e.major.to(), e.minor.to(), e.patch.to());
714                self.features.observe_version(version);
715                vec![StateEvents::Exchange(ExchangeEvent::ContractVersionUpdated(version))]
716            },
717            ExchangeEvents::CrossesBook(_) => self
718                .err_ctx(ctx, event)?
719                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::CrossesBook))
720                .into_iter()
721                .collect(),
722            ExchangeEvents::DcpBorrowThreshUpdated(_) => vec![],
723            ExchangeEvents::DecreaseCollateralBeyondMarkPrice(_) => vec![],
724            // The unit these rates arrive in follows the deployed version, and the
725            // contract publishes `ContractVersionSet` BEFORE any schedule event in the
726            // upgrade transaction itself -- so reading `self.features` as the event is
727            // handled already reflects the cutover.
728            ExchangeEvents::DefaultPerpFeeScheduleSet(e) => self.update_fee_schedule(
729                instant,
730                FeeSchedule::new(
731                    FeeScheduleKey::Default,
732                    e.takerFeesPer100K,
733                    e.makerFeesPer100K,
734                    self.features.fee_rate_converter(),
735                ),
736            ),
737            ExchangeEvents::DefaultRwaFeeScheduleSet(e) => self.update_fee_schedule(
738                instant,
739                FeeSchedule::new(
740                    FeeScheduleKey::RwaDefault,
741                    e.takerFeesPer100K,
742                    e.makerFeesPer100K,
743                    self.features.fee_rate_converter(),
744                ),
745            ),
746            ExchangeEvents::DeleveragePositionListEmpty(_) => vec![],
747            ExchangeEvents::ExceedsLastExecutionBlock(_) => self
748                .err_ctx(ctx, event)?
749                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::ExceedsLastExecutionBlock))
750                .into_iter()
751                .collect(),
752            ExchangeEvents::ExchangeHalted(e) => {
753                self.is_halted = e.halted;
754                vec![StateEvents::Exchange(ExchangeEvent::Halted(self.is_halted))]
755            },
756            ExchangeEvents::ExchangeInitialized(_) => vec![],
757            ExchangeEvents::FeeParamsUpdated(_) => vec![],
758            ExchangeEvents::FeeScheduleSet(e) => {
759                // `setFeeSchedValues(id, …)` rewrites the rates of the schedule
760                // registered under `id`, whichever contracts happen to point at
761                // it. It does NOT repoint anything: a custom id names a schedule
762                // *keyed by* a perpetual's id, not that perpetual's current
763                // schedule - only `PerpFeeSchedIdSet` moves a contract onto it.
764                self.update_fee_schedule(
765                    instant,
766                    FeeSchedule::new(
767                        FeeScheduleKey::from_raw(e.feeSchedId),
768                        e.takerFeesPer100K,
769                        e.makerFeesPer100K,
770                        self.features.fee_rate_converter(),
771                    ),
772                )
773            },
774            // Witnesses of the v1.1.7.5 fee-unit redenomination, carried for
775            // completeness rather than state: `initializeV4` emits the id-appropriate
776            // `*FeeScheduleSet` alongside every `FeeScheduleMigrated`, and those arms
777            // are what move the registry -- already in the new unit, because
778            // `ContractVersionSet` came first in the same transaction.
779            ExchangeEvents::FeeScheduleMigrated(_) => vec![],
780            ExchangeEvents::FeeUnitRedenominated(_) => vec![],
781            ExchangeEvents::FundingClampPctUpdated(_) => vec![],
782            ExchangeEvents::FundingEventCompleted(e) => {
783                if let Some(perp) = self.perpetual(e.perpId) {
784                    perp.update_funding(
785                        instant,
786                        perp.funding_rate_converter()
787                            .from_signed(e.actualRatePct100k),
788                        perp.funding_sum_converter()
789                            .from_i64(e.fundingPaymentPNS.as_i64()),
790                        e.fundingEventBlock.to(),
791                    );
792                }
793                vec![]
794            },
795            ExchangeEvents::FundingEventSetTooEarly(_) => vec![],
796            ExchangeEvents::FundingPriceExceedsTol(_) => vec![],
797            ExchangeEvents::FundingSumAlreadySet(_) => vec![],
798            ExchangeEvents::FundingSumScalingExpUpdated(e) => self
799                .perpetual(e.perpId)
800                .map(|perp| {
801                    perp.update_funding_sum_scaling_exp(instant, e.newExp.to());
802                    StateEvents::perpetual(
803                        perp,
804                        PerpetualEventType::FundingSumScalingExpUpdated(e.newExp.to()),
805                    )
806                })
807                .into_iter()
808                .collect(),
809            ExchangeEvents::IgnoreOracleUpdated(e) => self
810                .perpetual(e.perpId)
811                .map(|perp| {
812                    perp.update_is_oracle_used(instant, !e.ignOracle);
813                    StateEvents::perpetual(
814                        perp,
815                        PerpetualEventType::OracleConfigurationUpdated {
816                            is_used: perp.is_oracle_used(),
817                            feed_id: perp.oracle_feed_id(),
818                        },
819                    )
820                })
821                .into_iter()
822                .collect(),
823            ExchangeEvents::ImmediateOrCancelExecuted(_) => self
824                .err_ctx(ctx, event)?
825                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::ImmediateOrCancelExecuted))
826                .into_iter()
827                .collect(),
828            ExchangeEvents::IncreasePositionCollateral(e) => chain!(
829                self.position(e.accountId, e.perpId)?.map(|(pos, _)| {
830                    pos.update_deposit(instant, cc.from_unsigned(e.positionDepositCNS));
831                    StateEvents::position(
832                        pos,
833                        ctx,
834                        PositionEventType::DepositUpdated(pos.deposit()),
835                    )
836                }),
837                self.account(e.accountId).map(|acc| {
838                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
839                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
840                }),
841            )
842            .collect(),
843            ExchangeEvents::Initialized(_) => vec![],
844            ExchangeEvents::InitialMarginFractionUpdated(e) => self
845                .perpetual(e.perpId)
846                .map(|perp| {
847                    perp.update_initial_margin(
848                        instant,
849                        perp.leverage_converter()
850                            .from_unsigned(e.initMarginFracHdths),
851                    );
852                    StateEvents::perpetual(
853                        perp,
854                        PerpetualEventType::InitialMarginFractionUpdated(perp.initial_margin()),
855                    )
856                })
857                .into_iter()
858                .collect(),
859            ExchangeEvents::InsolventPositionCannotBeForcedClose(_) => vec![],
860            ExchangeEvents::InsuficientFundsForRecycleFee(_) => self
861                .err_ctx(ctx, event)?
862                .map(|ctx| {
863                    StateEvents::order_error(ctx, OrderErrorType::InsuficientFundsForRecycleFee)
864                })
865                .into_iter()
866                .collect(),
867            ExchangeEvents::InsufficientFundsToDecCollateral(_) => vec![],
868            ExchangeEvents::InsurancePaymentForSettlement(_) => vec![],
869            ExchangeEvents::InvalidAccountFrozenOrder(_) => vec![],
870            ExchangeEvents::InvalidBankruptcyPrice(_) => vec![],
871            ExchangeEvents::InvalidExpiryBlock(_) => self
872                .err_ctx(ctx, event)?
873                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::InvalidExpiryBlock))
874                .into_iter()
875                .collect(),
876            ExchangeEvents::InvalidLinkReportForContract(_) => vec![],
877            ExchangeEvents::InvalidLinkReportVersion(_) => vec![],
878            ExchangeEvents::InvalidLiquidationPrice(_) => vec![],
879            ExchangeEvents::InvalidOrderId(_) => self
880                .err_ctx(ctx, event)?
881                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::InvalidOrderId))
882                .into_iter()
883                .collect(),
884            ExchangeEvents::LastForwardedDescIdReset(_) => vec![],
885            ExchangeEvents::LastTriggeredDescIdReset(_) => vec![],
886            ExchangeEvents::LinkDatastreamConfigured(_) => vec![],
887            ExchangeEvents::LinkDsError_0(_) => vec![],
888            ExchangeEvents::LinkDsError_1(_) => vec![],
889            ExchangeEvents::LinkDsPanic(_) => vec![],
890            ExchangeEvents::LinkPriceUpdated(e) => self
891                .perpetual(e.perpId)
892                .map(|perp| {
893                    perp.update_oracle_price(
894                        instant,
895                        perp.price_converter().from_unsigned(e.oraclePricePNS),
896                    );
897                    StateEvents::perpetual(
898                        perp,
899                        PerpetualEventType::OraclePriceUpdated(perp.oracle_price()),
900                    )
901                })
902                .into_iter()
903                .collect(),
904            ExchangeEvents::LiquidationBuyerUpdated(_) => vec![],
905            ExchangeEvents::LiquidationParamsUpdated(_) => vec![],
906            ExchangeEvents::LotOutOfRange(_) => self
907                .err_ctx(ctx, event)?
908                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::SizeOutOfRange))
909                .into_iter()
910                .collect(),
911            ExchangeEvents::MaintenanceMarginFractionUpdated(e) => self
912                .perpetual(e.perpId)
913                .map(|perp| {
914                    perp.update_maintenance_margin(
915                        instant,
916                        perp.leverage_converter()
917                            .from_unsigned(e.maintMarginFracHdths),
918                    );
919                    StateEvents::perpetual(
920                        perp,
921                        PerpetualEventType::MaintenanceMarginFractionUpdated(
922                            perp.maintenance_margin(),
923                        ),
924                    )
925                })
926                .into_iter()
927                .collect(),
928            // Deprecated in v1.1.7.4, replayed from earlier history only
929            ExchangeEvents::MakerFeeUpdated(e) => self
930                .perpetual(e.perpId)
931                .map(|perp| {
932                    perp.update_base_maker_fee(
933                        instant,
934                        perp.fee_converter().from_unsigned(e.makerFeePer100K),
935                    );
936                    StateEvents::perpetual(
937                        perp,
938                        PerpetualEventType::MakerFeeUpdated(perp.maker_fee()),
939                    )
940                })
941                .into_iter()
942                .collect(),
943            // Superseded by `MakerOrderFilledV2` in v1.1.7.4, replayed from
944            // earlier history only, hence no builder fee
945            ExchangeEvents::MakerOrderFilled(e) => self.apply_maker_order_filled(
946                instant,
947                event,
948                ctx,
949                RawMakerFill {
950                    perp_id: e.perpId,
951                    account_id: e.accountId,
952                    order_id: e.orderId,
953                    price_pns: e.pricePNS,
954                    lot_lns: e.lotLNS,
955                    fee_cns: e.feeCNS,
956                    builder_fee_cns: U256::ZERO,
957                    locked_balance_cns: e.lockedBalanceCNS,
958                    balance_cns: e.balanceCNS,
959                },
960            )?,
961            ExchangeEvents::MakerOrderFilledV2(e) => self.apply_maker_order_filled(
962                instant,
963                event,
964                ctx,
965                RawMakerFill {
966                    perp_id: e.perpId,
967                    account_id: e.accountId,
968                    order_id: e.orderId,
969                    price_pns: e.pricePNS,
970                    lot_lns: e.lotLNS,
971                    fee_cns: e.feeCNS,
972                    builder_fee_cns: e.builderFeeCNS,
973                    locked_balance_cns: e.lockedBalanceCNS,
974                    balance_cns: e.balanceCNS,
975                },
976            )?,
977            ExchangeEvents::MakerOrderSettlementFailed(e) => chain!(
978                if let Some(perp) = self.perpetual(e.perpId) {
979                    let order_id = std::num::NonZeroU16::new(e.orderId.to::<u16>())
980                        .expect("orderId in event cannot be 0");
981                    let order = perp.remove_order(order_id)?;
982                    chain!(
983                        Some(StateEvents::order(perp, &order, ctx, OrderEventType::Removed)),
984                        self.err_ctx(ctx, event)?
985                            .map(|ctx| StateEvents::affected_order_error(
986                                ctx,
987                                &order,
988                                OrderErrorType::MakerOrderSettlementFailed
989                            ))
990                    )
991                    .collect()
992                } else {
993                    vec![]
994                },
995                self.account(e.accountId).map(|acc| {
996                    acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
997                    StateEvents::account(
998                        acc,
999                        ctx,
1000                        AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
1001                    )
1002                }),
1003                if !e.recyclerAmountCNS.is_zero() {
1004                    self.account(e.recyclerAccountId).map(|acc| {
1005                        acc.update_balance(instant, cc.from_unsigned(e.recyclerBalanceCNS));
1006                        StateEvents::account(
1007                            acc,
1008                            ctx,
1009                            AccountEventType::BalanceUpdated(acc.balance()),
1010                        )
1011                    })
1012                } else {
1013                    None
1014                },
1015            )
1016            .collect(),
1017            ExchangeEvents::MarginTolUpdated(_) => vec![],
1018            ExchangeEvents::MarkExceedsTol(_) => vec![],
1019            ExchangeEvents::MarkPriceAgeExceedsMax(_) => vec![],
1020            ExchangeEvents::MarkUpdated(e) => {
1021                let perp_mark = self.perpetual(e.perpId).map(|perp| {
1022                    perp.update_mark_price(
1023                        instant,
1024                        perp.price_converter().from_unsigned(e.pricePNS),
1025                    );
1026                    (perp.id(), perp.mark_price())
1027                });
1028                if let Some((perp_id, mark_price)) = perp_mark {
1029                    chain!(
1030                        Some(StateEvents::Perpetual(PerpetualEvent {
1031                            perpetual_id: perp_id,
1032                            r#type: PerpetualEventType::MarkPriceUpdated(mark_price),
1033                        })),
1034                        // Applying updated mark to all tracked positions
1035                        self.accounts.values_mut().filter_map(|acc| {
1036                            acc.positions_mut().get_mut(&perp_id).map(|pos| {
1037                                pos.apply_mark_price(instant, mark_price);
1038                                StateEvents::position(
1039                                    pos,
1040                                    &None,
1041                                    PositionEventType::UnrealizedPnLUpdated {
1042                                        pnl: pos.pnl(),
1043                                        delta_pnl: pos.delta_pnl(),
1044                                        premium_pnl: pos.premium_pnl(),
1045                                    },
1046                                )
1047                            })
1048                        }),
1049                    )
1050                    .collect()
1051                } else {
1052                    vec![]
1053                }
1054            },
1055            ExchangeEvents::MaxMatchesReached(_) => self
1056                .err_ctx(ctx, event)?
1057                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::MaxMatchesReached))
1058                .into_iter()
1059                .collect(),
1060            ExchangeEvents::MaxOpenInterestUpdated(_) => vec![],
1061            ExchangeEvents::MaximumAccountOrders(_) => self
1062                .err_ctx(ctx, event)?
1063                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::MaximumAccountOrders))
1064                .into_iter()
1065                .collect(),
1066            ExchangeEvents::MinAccountOpenAmountUpdated(_) => vec![],
1067            ExchangeEvents::MinPostUpdated(e) => {
1068                self.min_post = cc.from_unsigned(e.minPostCNS);
1069                vec![StateEvents::Exchange(ExchangeEvent::MinPostUpdated(self.min_post))]
1070            },
1071            ExchangeEvents::MinSettleUpdated(e) => {
1072                self.min_settle = cc.from_unsigned(e.minSettleCNS);
1073                vec![StateEvents::Exchange(ExchangeEvent::MinSettleUpdated(self.min_settle))]
1074            },
1075            ExchangeEvents::MonitorAdministratorUpdated(_) => vec![],
1076            ExchangeEvents::MonitorPauseAttempted(_) => vec![],
1077            ExchangeEvents::OracleAgeExceedsMax(_) => vec![],
1078            ExchangeEvents::OracleDisabled(_) => vec![],
1079            ExchangeEvents::OrderBatchCompleted(_) => {
1080                // Reset context
1081                ctx.take();
1082                vec![]
1083            },
1084            ExchangeEvents::OrderCancelled(e) => {
1085                let c = must_ctx()?;
1086                let order_id = c.order_id.expect("order_id required for OrderCancelled");
1087                chain!(
1088                    if let Some(perp) = self.perpetuals.get_mut(&c.perpetual_id) {
1089                        let order = perp.remove_order(order_id)?;
1090                        Some(StateEvents::order(perp, &order, ctx, OrderEventType::Removed))
1091                    } else {
1092                        None
1093                    },
1094                    if let Some(acc) = self.accounts.get_mut(&c.account_id) {
1095                        acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
1096                        acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
1097                        vec![
1098                            StateEvents::account(
1099                                acc,
1100                                ctx,
1101                                AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
1102                            ),
1103                            StateEvents::account(
1104                                acc,
1105                                ctx,
1106                                AccountEventType::BalanceUpdated(acc.balance()),
1107                            ),
1108                        ]
1109                    } else {
1110                        vec![]
1111                    },
1112                )
1113                .collect()
1114            },
1115            ExchangeEvents::OrderCancelledByAdmin(e) => chain!(
1116                self.order(e.perpId, e.orderId)?.map(|(perp, order)| {
1117                    perp.remove_order(order.order_id()).expect("order exists");
1118                    StateEvents::order(perp, &order, ctx, OrderEventType::Removed)
1119                }),
1120                self.account(e.accountId).map(|acc| {
1121                    acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
1122                    StateEvents::account(
1123                        acc,
1124                        ctx,
1125                        AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
1126                    )
1127                }),
1128            )
1129            .collect(),
1130            ExchangeEvents::OrderCancelledByLiquidator(e) => chain!(
1131                self.order(e.perpId, e.orderId)?.map(|(perp, order)| {
1132                    perp.remove_order(order.order_id()).expect("order exists");
1133                    StateEvents::order(perp, &order, ctx, OrderEventType::Removed)
1134                }),
1135                self.account(e.accountId).map(|acc| {
1136                    acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
1137                    StateEvents::account(
1138                        acc,
1139                        ctx,
1140                        AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
1141                    )
1142                }),
1143            )
1144            .collect(),
1145            ExchangeEvents::OrderChanged(e) => {
1146                let c = must_ctx()?;
1147                let order_id = c.order_id.expect("order_id required for OrderChanged");
1148                chain!(
1149                    if let Some(perp) = self.perpetuals.get_mut(&c.perpetual_id) {
1150                        let order = perp
1151                            .get_order(order_id)
1152                            .copied()
1153                            .ok_or(DexError::OrderNotFound(perp.id(), order_id))?;
1154                        let new_price = perp.price_converter().from_unsigned(e.pricePNS);
1155                        let new_size = perp.size_converter().from_unsigned(e.lotLNS);
1156                        let new_expiry_block = e.expiryBlock.to();
1157                        let price_update =
1158                            if order.price() != new_price { Some(new_price) } else { None };
1159                        let size_update =
1160                            if order.size() != new_size { Some(new_size) } else { None };
1161                        let expiry_block_update = if order.expiry_block() != new_expiry_block {
1162                            Some(new_expiry_block)
1163                        } else {
1164                            None
1165                        };
1166                        let updated = order.updated(
1167                            instant,
1168                            ctx,
1169                            price_update,
1170                            size_update,
1171                            size_update,
1172                            expiry_block_update,
1173                        );
1174                        perp.update_order(updated)?;
1175                        Some(StateEvents::order(
1176                            perp,
1177                            &order,
1178                            ctx,
1179                            OrderEventType::Updated {
1180                                price: price_update,
1181                                size: size_update,
1182                                expiry_block: expiry_block_update,
1183                            },
1184                        ))
1185                    } else {
1186                        None
1187                    },
1188                    if let Some(acc) = self.accounts.get_mut(&c.account_id) {
1189                        acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
1190                        acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
1191                        vec![
1192                            StateEvents::account(
1193                                acc,
1194                                ctx,
1195                                AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
1196                            ),
1197                            StateEvents::account(
1198                                acc,
1199                                ctx,
1200                                AccountEventType::BalanceUpdated(acc.balance()),
1201                            ),
1202                        ]
1203                    } else {
1204                        vec![]
1205                    },
1206                )
1207                .collect()
1208            },
1209            ExchangeEvents::OrderDescIdTooLow(_) => vec![],
1210            ExchangeEvents::OrderDoesNotExist(_) => self
1211                .err_ctx(ctx, event)?
1212                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::OrderDoesNotExist))
1213                .into_iter()
1214                .collect(),
1215            ExchangeEvents::OrderExtensionRejected(_) => self
1216                .err_ctx(ctx, event)?
1217                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::OrderExtensionRejected))
1218                .into_iter()
1219                .collect(),
1220            ExchangeEvents::OrderForwardingNotAllowed(_) => vec![],
1221            ExchangeEvents::OrderForwardingUpdated(_) => vec![],
1222            ExchangeEvents::OrderPlaced(e) => {
1223                let c = must_ctx()?;
1224                let order_id = std::num::NonZeroU16::new(e.orderId.to::<u16>())
1225                    .expect("orderId in OrderPlaced event cannot be 0");
1226                chain!(
1227                    if let Some(perp) = self.perpetuals.get_mut(&c.perpetual_id) {
1228                        let order = Order::placed(
1229                            instant,
1230                            c,
1231                            order_id,
1232                            perp.size_converter().from_unsigned(e.lotLNS),
1233                            perp.price_converter(),
1234                            perp.leverage_converter(),
1235                        );
1236                        let event = OrderEventType::Placed {
1237                            r#type: order.r#type(),
1238                            price: order.price(),
1239                            size: order.size(),
1240                            expiry_block: order.expiry_block(),
1241                            leverage: order.leverage(),
1242                            post_only: order.post_only().unwrap_or_default(),
1243                            fill_or_kill: order.fill_or_kill().unwrap_or_default(),
1244                            immediate_or_cancel: order.immediate_or_cancel().unwrap_or_default(),
1245                        };
1246                        perp.add_order(order)?;
1247                        Some(StateEvents::order(perp, &order, ctx, event))
1248                    } else {
1249                        None
1250                    },
1251                    if let Some(acc) = self.accounts.get_mut(&c.account_id) {
1252                        acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
1253                        acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
1254                        vec![
1255                            StateEvents::account(
1256                                acc,
1257                                ctx,
1258                                AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
1259                            ),
1260                            StateEvents::account(
1261                                acc,
1262                                ctx,
1263                                AccountEventType::BalanceUpdated(acc.balance()),
1264                            ),
1265                        ]
1266                    } else {
1267                        vec![]
1268                    },
1269                )
1270                .collect()
1271            },
1272            ExchangeEvents::OrderPostFailed(e) => self
1273                .err_ctx(ctx, event)?
1274                .map(|ctx| {
1275                    StateEvents::order_error(ctx, OrderErrorType::OrderPostFailed(e.reason.to()))
1276                })
1277                .into_iter()
1278                .collect(),
1279            // Superseded by `OrderRequestV2` in v1.1.7.4, replayed from earlier
1280            // history only
1281            ExchangeEvents::OrderRequest(e) => {
1282                // Store order request context as it is required to handle
1283                // future events
1284                ctx.replace(OrderContext::from(e));
1285                vec![]
1286            },
1287            ExchangeEvents::OrderRequestV2(e) => {
1288                ctx.replace(OrderContext::from(e));
1289                vec![]
1290            },
1291            ExchangeEvents::OrderSettlementImpliesInsolvent(_) => self
1292                .err_ctx(ctx, event)?
1293                .map(|ctx| {
1294                    StateEvents::order_error(ctx, OrderErrorType::OrderSettlementImpliesInsolvent)
1295                })
1296                .into_iter()
1297                .collect(),
1298            ExchangeEvents::OrderSizeExceedsAvailableSize(_) => self
1299                .err_ctx(ctx, event)?
1300                .map(|ctx| {
1301                    StateEvents::order_error(ctx, OrderErrorType::OrderSizeExceedsAvailableSize)
1302                })
1303                .into_iter()
1304                .collect(),
1305            ExchangeEvents::OverCollatDescentThreshUpdated(_) => vec![],
1306            ExchangeEvents::OwnershipTransferStarted(_) => vec![],
1307            ExchangeEvents::OwnershipTransferred(_) => vec![],
1308            ExchangeEvents::PermissonedCancelParamsUpdated(_) => vec![],
1309            ExchangeEvents::PerpFeeSchedIdSet(e) => {
1310                // The only event that repoints a contract at another schedule.
1311                // Id-only, so the rates come from the registry entry now pointed
1312                // at.
1313                //
1314                // A schedule never observed has no rates to resolve: the id is
1315                // applied on its own and the `FeeScheduleSet` that eventually
1316                // writes it reports the rates, rather than surfacing an
1317                // intermediate schedule pairing the new id with the old rates -
1318                // a state that never applies to a fill.
1319                let key = FeeScheduleKey::from_raw(e.feeSchedId);
1320                let schedule = self.fee_schedules.get(key);
1321                self.perpetual(e.perpId)
1322                    .and_then(|perp| match schedule {
1323                        Some(schedule) => {
1324                            perp.update_fee_schedule(instant, schedule);
1325                            Some(StateEvents::perpetual(
1326                                perp,
1327                                PerpetualEventType::FeeScheduleUpdated(perp.fee_schedule()),
1328                            ))
1329                        },
1330                        None => {
1331                            perp.update_fee_schedule_key(instant, key);
1332                            None
1333                        },
1334                    })
1335                    .into_iter()
1336                    .collect()
1337            },
1338            ExchangeEvents::PerpPositionBalCreditPositiveSevere(_) => vec![],
1339            ExchangeEvents::PositionAdministratorUpdated(_) => vec![],
1340            ExchangeEvents::PositionClosed(e) => {
1341                if let Some(ctx) = ctx {
1342                    ctx.position_closed_at_log_index = Some(event.log_index());
1343                }
1344
1345                if let Some((acc, perp)) = self.account_perpetual(e.accountId, e.perpId) {
1346                    let pos = acc
1347                        .positions_mut()
1348                        .remove(&perp.id())
1349                        .ok_or(DexError::PositionNotFound(acc.id(), perp.id()))?;
1350
1351                    chain!(
1352                        Some(StateEvents::position(
1353                            &pos,
1354                            ctx,
1355                            PositionEventType::Closed {
1356                                r#type: pos.r#type(),
1357                                entry_price: pos.entry_price(),
1358                                exit_price: perp.price_converter().from_unsigned(e.pricePNS),
1359                                size: pos.size(),
1360                                delta_pnl: cc.from_signed(e.deltaPnlCNS),
1361                                premium_pnl: cc.from_signed(e.fundingCNS),
1362                            }
1363                        )),
1364                        if PositionType::from(e.positionType) == PositionType::Long {
1365                            perp.update_open_interest(instant, pos.size(), UD64::ZERO);
1366                            Some(StateEvents::perpetual(
1367                                perp,
1368                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1369                            ))
1370                        } else {
1371                            None
1372                        },
1373                    )
1374                    .collect()
1375                } else {
1376                    vec![]
1377                }
1378            },
1379            ExchangeEvents::PositionCollateralDecreased(e) => {
1380                if let Some((pos, perp)) = self.position(e.accountId, e.perpId)? {
1381                    let prev_entry_price = pos.entry_price();
1382                    pos.update_entry_price(instant, e.endEntryPricePNS, 0, perp.price_converter());
1383                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1384                    pos.apply_mark_price(instant, perp.mark_price());
1385                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1386                    vec![StateEvents::position(
1387                        pos,
1388                        ctx,
1389                        PositionEventType::CollateralDecreased {
1390                            prev_entry_price,
1391                            new_entry_price: pos.entry_price(),
1392                            deposit: pos.deposit(),
1393                        },
1394                    )]
1395                } else {
1396                    vec![]
1397                }
1398            },
1399            ExchangeEvents::PositionDecreased(e) => {
1400                if let Some((pos, perp)) = self.position(e.accountId, e.perpId)? {
1401                    let prev_size = pos.size();
1402                    pos.update_size(instant, perp.size_converter().from_unsigned(e.endLotLNS));
1403                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1404                    pos.apply_mark_price(instant, perp.mark_price());
1405                    pos.update_premium_pnl(
1406                        instant,
1407                        pos.premium_pnl().sub(cc.from_signed(e.fundingCNS)),
1408                    );
1409                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1410                    chain!(
1411                        Some(StateEvents::position(
1412                            pos,
1413                            ctx,
1414                            PositionEventType::Decreased {
1415                                prev_size,
1416                                new_size: pos.size(),
1417                                deposit: pos.deposit(),
1418                                delta_pnl: pos.delta_pnl(),
1419                                premium_pnl: pos.premium_pnl(),
1420                            }
1421                        )),
1422                        if pos.r#type() == PositionType::Long {
1423                            perp.update_open_interest(instant, prev_size, pos.size());
1424                            Some(StateEvents::perpetual(
1425                                perp,
1426                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1427                            ))
1428                        } else {
1429                            None
1430                        },
1431                    )
1432                    .collect()
1433                } else {
1434                    vec![]
1435                }
1436            },
1437            ExchangeEvents::PositionDeleveraged(e) => chain!(
1438                if let Some((pos, perp)) = self.position(e.accountId, e.perpId)? {
1439                    let prev_size = pos.size();
1440                    pos.update_size(instant, perp.size_converter().from_unsigned(e.endLotLNS));
1441                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1442                    pos.apply_mark_price(instant, perp.mark_price());
1443                    pos.update_premium_pnl(
1444                        instant,
1445                        pos.premium_pnl().sub(cc.from_signed(e.fundingCNS)),
1446                    );
1447                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1448                    chain!(
1449                        Some(StateEvents::position(
1450                            pos,
1451                            ctx,
1452                            PositionEventType::Deleveraged {
1453                                force_close: e.forceClose,
1454                                r#type: pos.r#type(),
1455                                entry_price: pos.entry_price(),
1456                                exit_price: perp
1457                                    .price_converter()
1458                                    .from_unsigned(e.deleveragePricePNS),
1459                                prev_size,
1460                                new_size: pos.size(),
1461                                deposit: pos.deposit(),
1462                                delta_pnl: pos.delta_pnl(),
1463                                premium_pnl: pos.premium_pnl(),
1464                            }
1465                        )),
1466                        if pos.r#type() == PositionType::Long {
1467                            perp.update_open_interest(instant, prev_size, pos.size());
1468                            Some(StateEvents::perpetual(
1469                                perp,
1470                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1471                            ))
1472                        } else {
1473                            None
1474                        },
1475                    )
1476                    .collect()
1477                } else {
1478                    vec![]
1479                },
1480                self.account(e.accountId).map(|acc| {
1481                    if e.endLotLNS == U256::ZERO {
1482                        acc.positions_mut()
1483                            .remove(&e.perpId.to::<types::PerpetualId>());
1484                    }
1485                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
1486                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
1487                }),
1488            )
1489            .collect(),
1490            ExchangeEvents::PositionDeleveragedV2(e) => chain!(
1491                if let Some((pos, perp)) = self.position(e.accountId, e.perpId)? {
1492                    let prev_size = pos.size();
1493                    pos.update_size(instant, perp.size_converter().from_unsigned(e.endLotLNS));
1494                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1495                    pos.apply_mark_price(instant, perp.mark_price());
1496                    pos.update_premium_pnl(
1497                        instant,
1498                        pos.premium_pnl().sub(cc.from_signed(e.fundingCNS)),
1499                    );
1500                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1501                    chain!(
1502                        Some(StateEvents::position(
1503                            pos,
1504                            ctx,
1505                            PositionEventType::Deleveraged {
1506                                force_close: e.forceClose,
1507                                r#type: pos.r#type(),
1508                                entry_price: pos.entry_price(),
1509                                exit_price: perp
1510                                    .price_converter()
1511                                    .from_unsigned(e.deleveragePricePNS),
1512                                prev_size,
1513                                new_size: pos.size(),
1514                                deposit: pos.deposit(),
1515                                delta_pnl: pos.delta_pnl(),
1516                                premium_pnl: pos.premium_pnl(),
1517                            }
1518                        )),
1519                        if pos.r#type() == PositionType::Long {
1520                            perp.update_open_interest(instant, prev_size, pos.size());
1521                            Some(StateEvents::perpetual(
1522                                perp,
1523                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1524                            ))
1525                        } else {
1526                            None
1527                        },
1528                    )
1529                    .collect()
1530                } else {
1531                    vec![]
1532                },
1533                self.account(e.accountId).map(|acc| {
1534                    if e.endLotLNS == U256::ZERO {
1535                        acc.positions_mut()
1536                            .remove(&e.perpId.to::<types::PerpetualId>());
1537                    }
1538                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
1539                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
1540                }),
1541            )
1542            .collect(),
1543            ExchangeEvents::PositionDoesNotExist(_) => vec![],
1544            ExchangeEvents::PositionIncreased(e) => {
1545                if let Some((pos, perp)) = self.position(e.accountId, e.perpId)? {
1546                    let prev_size = pos.size();
1547                    pos.update_entry_price(instant, e.pricePNS, 0, perp.price_converter());
1548                    pos.update_size(instant, perp.size_converter().from_unsigned(e.endLotLNS));
1549                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1550                    pos.apply_mark_price(instant, perp.mark_price());
1551                    pos.update_premium_pnl(instant, D256::ZERO);
1552                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1553
1554                    chain!(
1555                        Some(StateEvents::position(
1556                            pos,
1557                            ctx,
1558                            PositionEventType::Increased {
1559                                entry_price: pos.entry_price(),
1560                                prev_size,
1561                                new_size: pos.size(),
1562                                deposit: pos.deposit(),
1563                            }
1564                        )),
1565                        if pos.r#type() == PositionType::Long {
1566                            perp.update_open_interest(instant, prev_size, pos.size());
1567                            Some(StateEvents::perpetual(
1568                                perp,
1569                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1570                            ))
1571                        } else {
1572                            None
1573                        },
1574                    )
1575                    .collect()
1576                } else {
1577                    vec![]
1578                }
1579            },
1580            ExchangeEvents::PositionIncreasedV2(e) => {
1581                if let Some((pos, perp)) = self.position(e.accountId, e.perpId)? {
1582                    let prev_size = pos.size();
1583                    pos.update_entry_price(
1584                        instant,
1585                        e.pricePNS,
1586                        e.priceResiduePNSQ16.to(),
1587                        perp.price_converter(),
1588                    );
1589                    pos.update_size(instant, perp.size_converter().from_unsigned(e.endLotLNS));
1590                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1591                    pos.apply_mark_price(instant, perp.mark_price());
1592                    pos.update_premium_pnl(instant, D256::ZERO);
1593                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1594
1595                    chain!(
1596                        Some(StateEvents::position(
1597                            pos,
1598                            ctx,
1599                            PositionEventType::Increased {
1600                                entry_price: pos.entry_price(),
1601                                prev_size,
1602                                new_size: pos.size(),
1603                                deposit: pos.deposit(),
1604                            }
1605                        )),
1606                        if pos.r#type() == PositionType::Long {
1607                            perp.update_open_interest(instant, prev_size, pos.size());
1608                            Some(StateEvents::perpetual(
1609                                perp,
1610                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1611                            ))
1612                        } else {
1613                            None
1614                        },
1615                    )
1616                    .collect()
1617                } else {
1618                    vec![]
1619                }
1620            },
1621            ExchangeEvents::PositionInverted(e) => {
1622                if let Some((pos, perp)) = self.position(e.accountId, e.perpId)? {
1623                    let prev_type = pos.r#type();
1624                    let prev_entry_price = pos.entry_price();
1625                    let prev_size = pos.size();
1626                    pos.update_type(instant, PositionType::from(e.positionType));
1627                    pos.update_entry_price(instant, e.pricePNS, 0, perp.price_converter());
1628                    pos.update_size(instant, perp.size_converter().from_unsigned(e.endLotLNS));
1629                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1630                    pos.apply_mark_price(instant, perp.mark_price());
1631                    pos.update_premium_pnl(instant, D256::ZERO);
1632                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1633                    if pos.r#type() == PositionType::Long {
1634                        perp.update_open_interest(instant, UD64::ZERO, pos.size());
1635                    } else {
1636                        perp.update_open_interest(instant, prev_size, UD64::ZERO);
1637                    }
1638                    vec![
1639                        StateEvents::position(
1640                            pos,
1641                            ctx,
1642                            PositionEventType::Closed {
1643                                r#type: prev_type,
1644                                entry_price: prev_entry_price,
1645                                exit_price: pos.entry_price(),
1646                                size: prev_size,
1647                                delta_pnl: cc.from_signed(e.deltaPnlCNS),
1648                                premium_pnl: cc.from_signed(e.fundingCNS),
1649                            },
1650                        ),
1651                        StateEvents::position(
1652                            pos,
1653                            ctx,
1654                            PositionEventType::Inverted {
1655                                r#type: pos.r#type(),
1656                                entry_price: pos.entry_price(),
1657                                prev_size,
1658                                new_size: pos.size(),
1659                                deposit: pos.deposit(),
1660                                delta_pnl: pos.delta_pnl(),
1661                                premium_pnl: pos.premium_pnl(),
1662                            },
1663                        ),
1664                        StateEvents::perpetual(
1665                            perp,
1666                            PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1667                        ),
1668                    ]
1669                } else {
1670                    vec![]
1671                }
1672            },
1673            ExchangeEvents::PositionLiquidated(e) => chain!(
1674                if let Some((pos, perp)) = self.position(e.posAccountId, e.perpId)? {
1675                    let prev_size = pos.size();
1676                    pos.update_size(instant, perp.size_converter().from_unsigned(e.posLotLNS));
1677                    pos.update_deposit(instant, cc.from_unsigned(e.posDepositCNS));
1678                    pos.apply_mark_price(instant, perp.mark_price());
1679                    pos.update_premium_pnl(
1680                        instant,
1681                        pos.premium_pnl().sub(cc.from_signed(e.fundingCNS)),
1682                    );
1683                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1684                    chain!(
1685                        Some(StateEvents::position(
1686                            pos,
1687                            ctx,
1688                            PositionEventType::Liquidated {
1689                                r#type: pos.r#type(),
1690                                entry_price: pos.entry_price(),
1691                                exit_price: perp.price_converter().from_unsigned(e.liqPricePNS),
1692                                prev_size,
1693                                liquidated_size: perp.size_converter().from_unsigned(e.liqLotLNS),
1694                                new_size: pos.size(),
1695                                deposit: pos.deposit(),
1696                                delta_pnl: pos.delta_pnl(),
1697                                premium_pnl: pos.premium_pnl(),
1698                            }
1699                        )),
1700                        if pos.r#type() == PositionType::Long {
1701                            perp.update_open_interest(instant, prev_size, pos.size());
1702                            Some(StateEvents::perpetual(
1703                                perp,
1704                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1705                            ))
1706                        } else {
1707                            None
1708                        },
1709                    )
1710                    .collect()
1711                } else {
1712                    vec![]
1713                },
1714                self.account(e.posAccountId).map(|acc| {
1715                    if e.posLotLNS == U256::ZERO {
1716                        acc.positions_mut()
1717                            .remove(&e.perpId.to::<types::PerpetualId>());
1718                    }
1719                    acc.update_balance(instant, cc.from_unsigned(e.accBalanceCNS));
1720                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
1721                }),
1722            )
1723            .collect(),
1724            ExchangeEvents::PositionLiquidationCredit(e) => self
1725                .position(e.accountId, e.perpId)?
1726                .map(|(pos, _)| {
1727                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1728                    StateEvents::position(
1729                        pos,
1730                        ctx,
1731                        PositionEventType::DepositUpdated(pos.deposit()),
1732                    )
1733                })
1734                .into_iter()
1735                .collect(),
1736            ExchangeEvents::PositionOpened(e) => {
1737                if let Some((acc, perp)) = self.account_perpetual(e.accountId, e.perpId) {
1738                    let pos = Position::opened(
1739                        instant,
1740                        perp.id(),
1741                        acc.id(),
1742                        PositionType::from(e.positionType),
1743                        e.pricePNS,
1744                        0,
1745                        perp.price_converter(),
1746                        perp.size_converter().from_unsigned(e.lotLNS),
1747                        cc.from_unsigned(e.depositCNS),
1748                        perp.maintenance_margin(),
1749                    );
1750                    let events = chain!(
1751                        Some(StateEvents::position(
1752                            &pos,
1753                            ctx,
1754                            PositionEventType::Opened {
1755                                r#type: pos.r#type(),
1756                                entry_price: pos.entry_price(),
1757                                size: pos.size(),
1758                                deposit: pos.deposit(),
1759                            }
1760                        )),
1761                        if pos.r#type() == PositionType::Long {
1762                            perp.update_open_interest(instant, UD64::ZERO, pos.size());
1763                            Some(StateEvents::perpetual(
1764                                perp,
1765                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1766                            ))
1767                        } else {
1768                            None
1769                        },
1770                    )
1771                    .collect();
1772                    acc.positions_mut().insert(perp.id(), pos);
1773                    events
1774                } else {
1775                    vec![]
1776                }
1777            },
1778            ExchangeEvents::PositionOpenedV2(e) => {
1779                if let Some((acc, perp)) = self.account_perpetual(e.accountId, e.perpId) {
1780                    let pos = Position::opened(
1781                        instant,
1782                        perp.id(),
1783                        acc.id(),
1784                        PositionType::from(e.positionType),
1785                        e.pricePNS,
1786                        e.priceResiduePNSQ16.to(),
1787                        perp.price_converter(),
1788                        perp.size_converter().from_unsigned(e.lotLNS),
1789                        cc.from_unsigned(e.depositCNS),
1790                        perp.maintenance_margin(),
1791                    );
1792                    let events = chain!(
1793                        Some(StateEvents::position(
1794                            &pos,
1795                            ctx,
1796                            PositionEventType::Opened {
1797                                r#type: pos.r#type(),
1798                                entry_price: pos.entry_price(),
1799                                size: pos.size(),
1800                                deposit: pos.deposit(),
1801                            }
1802                        )),
1803                        if pos.r#type() == PositionType::Long {
1804                            perp.update_open_interest(instant, UD64::ZERO, pos.size());
1805                            Some(StateEvents::perpetual(
1806                                perp,
1807                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1808                            ))
1809                        } else {
1810                            None
1811                        },
1812                    )
1813                    .collect();
1814                    acc.positions_mut().insert(perp.id(), pos);
1815                    events
1816                } else {
1817                    vec![]
1818                }
1819            },
1820            ExchangeEvents::PositionUnwound(e) => {
1821                if let Some((acc, perp)) = self.account_perpetual(e.accountId, e.perpId) {
1822                    let pos = acc
1823                        .positions_mut()
1824                        .remove(&perp.id())
1825                        .ok_or(DexError::PositionNotFound(acc.id(), perp.id()))?;
1826                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
1827                    chain!(
1828                        Some(StateEvents::position(
1829                            &pos,
1830                            ctx,
1831                            PositionEventType::Unwound {
1832                                r#type: pos.r#type(),
1833                                entry_price: pos.entry_price(),
1834                                exit_price: perp.price_converter().from_unsigned(e.pricePNS),
1835                                size: pos.size(),
1836                                fair_market_value: cc.from_signed(e.positionFmvCNS),
1837                                payment: cc.from_unsigned(e.paymentCNS),
1838                            }
1839                        )),
1840                        Some(StateEvents::account(
1841                            acc,
1842                            ctx,
1843                            AccountEventType::BalanceUpdated(acc.balance())
1844                        )),
1845                        if pos.r#type() == PositionType::Long {
1846                            perp.update_open_interest(instant, pos.size(), UD64::ZERO);
1847                            Some(StateEvents::perpetual(
1848                                perp,
1849                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1850                            ))
1851                        } else {
1852                            None
1853                        },
1854                    )
1855                    .collect()
1856                } else {
1857                    vec![]
1858                }
1859            },
1860            ExchangeEvents::PositionUnwoundV2(e) => {
1861                // Position is being removed; residue field is informational only.
1862                if let Some((acc, perp)) = self.account_perpetual(e.accountId, e.perpId) {
1863                    let pos = acc
1864                        .positions_mut()
1865                        .remove(&perp.id())
1866                        .ok_or(DexError::PositionNotFound(acc.id(), perp.id()))?;
1867                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
1868                    chain!(
1869                        Some(StateEvents::position(
1870                            &pos,
1871                            ctx,
1872                            PositionEventType::Unwound {
1873                                r#type: pos.r#type(),
1874                                entry_price: pos.entry_price(),
1875                                exit_price: perp.price_converter().from_unsigned(e.pricePNS),
1876                                size: pos.size(),
1877                                fair_market_value: cc.from_signed(e.positionFmvCNS),
1878                                payment: cc.from_unsigned(e.paymentCNS),
1879                            }
1880                        )),
1881                        Some(StateEvents::account(
1882                            acc,
1883                            ctx,
1884                            AccountEventType::BalanceUpdated(acc.balance())
1885                        )),
1886                        if pos.r#type() == PositionType::Long {
1887                            perp.update_open_interest(instant, pos.size(), UD64::ZERO);
1888                            Some(StateEvents::perpetual(
1889                                perp,
1890                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1891                            ))
1892                        } else {
1893                            None
1894                        },
1895                    )
1896                    .collect()
1897                } else {
1898                    vec![]
1899                }
1900            },
1901            ExchangeEvents::PositionUnwoundWithoutPayment(e) => {
1902                if let Some((acc, perp)) = self.account_perpetual(e.accountId, e.perpId) {
1903                    let pos = acc
1904                        .positions_mut()
1905                        .remove(&perp.id())
1906                        .ok_or(DexError::PositionNotFound(acc.id(), perp.id()))?;
1907                    chain!(
1908                        Some(StateEvents::position(
1909                            &pos,
1910                            ctx,
1911                            PositionEventType::Unwound {
1912                                r#type: pos.r#type(),
1913                                entry_price: pos.entry_price(),
1914                                exit_price: perp.price_converter().from_unsigned(e.pricePNS),
1915                                size: pos.size(),
1916                                fair_market_value: cc.from_signed(e.positionFmvCNS),
1917                                payment: UD128::ZERO,
1918                            }
1919                        )),
1920                        if pos.r#type() == PositionType::Long {
1921                            perp.update_open_interest(instant, pos.size(), UD64::ZERO);
1922                            Some(StateEvents::perpetual(
1923                                perp,
1924                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1925                            ))
1926                        } else {
1927                            None
1928                        },
1929                    )
1930                    .collect()
1931                } else {
1932                    vec![]
1933                }
1934            },
1935            ExchangeEvents::PositionUnwoundWithoutPaymentV2(e) => {
1936                // Position is being removed; residue field is informational only.
1937                if let Some((acc, perp)) = self.account_perpetual(e.accountId, e.perpId) {
1938                    let pos = acc
1939                        .positions_mut()
1940                        .remove(&perp.id())
1941                        .ok_or(DexError::PositionNotFound(acc.id(), perp.id()))?;
1942                    chain!(
1943                        Some(StateEvents::position(
1944                            &pos,
1945                            ctx,
1946                            PositionEventType::Unwound {
1947                                r#type: pos.r#type(),
1948                                entry_price: pos.entry_price(),
1949                                exit_price: perp.price_converter().from_unsigned(e.pricePNS),
1950                                size: pos.size(),
1951                                fair_market_value: cc.from_signed(e.positionFmvCNS),
1952                                payment: UD128::ZERO,
1953                            }
1954                        )),
1955                        if pos.r#type() == PositionType::Long {
1956                            perp.update_open_interest(instant, pos.size(), UD64::ZERO);
1957                            Some(StateEvents::perpetual(
1958                                perp,
1959                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1960                            ))
1961                        } else {
1962                            None
1963                        },
1964                    )
1965                    .collect()
1966                } else {
1967                    vec![]
1968                }
1969            },
1970            ExchangeEvents::PostOrderUnderMinimum(_) => self
1971                .err_ctx(ctx, event)?
1972                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::PostOrderUnderMinimum))
1973                .into_iter()
1974                .collect(),
1975            ExchangeEvents::PriceAdministratorUpdated(_) => vec![],
1976            ExchangeEvents::PriceMaxAgeUpdated(e) => {
1977                if let Some(perp) = self.perpetual(e.perpId) {
1978                    perp.update_price_max_age_sec(instant, e.maxAgeSec.to());
1979                }
1980                vec![]
1981            },
1982            ExchangeEvents::PriceOutOfRange(_) => self
1983                .err_ctx(ctx, event)
1984                .ok() // Used both for orders and mark/oracle prices
1985                .flatten()
1986                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::PriceOutOfRange))
1987                .into_iter()
1988                .collect(),
1989            ExchangeEvents::PriceSetDuringTriggerExec(_) => vec![],
1990            ExchangeEvents::PriceTolUpdated(_) => vec![],
1991            ExchangeEvents::ProtocolBalanceDeposit(_) => vec![],
1992            ExchangeEvents::ProtocolBalanceWithdraw(_) => vec![],
1993            ExchangeEvents::RecycleBalanceInsufficientSevere(_) => vec![],
1994            ExchangeEvents::RecycleFeeToAccount(_) => vec![],
1995            ExchangeEvents::RecycleFeeToProtocol(_) => vec![],
1996            ExchangeEvents::RecycleFeeUpdated(e) => {
1997                self.recycle_fee = cc.from_unsigned(e.recycleFeeCNS);
1998                vec![StateEvents::Exchange(ExchangeEvent::RecycleFeeUpdated(self.recycle_fee()))]
1999            },
2000            ExchangeEvents::ReportAgeExceedsLastUpdate(_) => vec![],
2001            ExchangeEvents::ReportExpiresTooSoon(_) => vec![],
2002            ExchangeEvents::ReportFromFuture(_) => vec![],
2003            ExchangeEvents::ReportPriceIsNegative(_) => vec![],
2004            ExchangeEvents::ResidueBalanceInsufficient(_) => vec![],
2005            ExchangeEvents::ResidueTransferred(_) => vec![],
2006            // Deprecated in v1.1.7.4, replayed from earlier history only
2007            ExchangeEvents::TakerFeeUpdated(e) => self
2008                .perpetual(e.perpId)
2009                .map(|perp| {
2010                    perp.update_base_taker_fee(
2011                        instant,
2012                        perp.fee_converter().from_unsigned(e.takerFeePer100K),
2013                    );
2014                    StateEvents::perpetual(
2015                        perp,
2016                        PerpetualEventType::TakerFeeUpdated(perp.taker_fee()),
2017                    )
2018                })
2019                .into_iter()
2020                .collect(),
2021            // Superseded by `TakerOrderFilledV2` in v1.1.7.4, replayed from
2022            // earlier history only, hence no builder fee
2023            ExchangeEvents::TakerOrderFilled(e) => self.apply_taker_order_filled(
2024                instant,
2025                event,
2026                ctx,
2027                RawTakerFill {
2028                    collat_price_pns: e.collatPricePNS,
2029                    lot_lns: e.lotLNS,
2030                    fee_cns: e.feeCNS,
2031                    builder_fee_cns: U256::ZERO,
2032                    balance_cns: e.balanceCNS,
2033                },
2034            )?,
2035            ExchangeEvents::TakerOrderFilledV2(e) => self.apply_taker_order_filled(
2036                instant,
2037                event,
2038                ctx,
2039                RawTakerFill {
2040                    collat_price_pns: e.collatPricePNS,
2041                    lot_lns: e.lotLNS,
2042                    fee_cns: e.feeCNS,
2043                    builder_fee_cns: e.builderFeeCNS,
2044                    balance_cns: e.balanceCNS,
2045                },
2046            )?,
2047            ExchangeEvents::ToleranceAdministratorUpdated(_) => vec![],
2048            ExchangeEvents::TransferAccountToProtocol(e) => self
2049                .account(e.accountId)
2050                .map(|acc| {
2051                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
2052                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
2053                })
2054                .into_iter()
2055                .collect(),
2056            ExchangeEvents::TransferPerpInsToProtocol(_) => vec![],
2057            ExchangeEvents::TransferPerpPosToProtocol(_) => vec![],
2058            ExchangeEvents::TransferProtocolToAccount(e) => self
2059                .account(e.accountId)
2060                .map(|acc| {
2061                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
2062                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
2063                })
2064                .into_iter()
2065                .collect(),
2066            ExchangeEvents::TransferProtocolToPerp(_) => vec![],
2067            ExchangeEvents::TransferProtocolToRecycleBal(_) => vec![],
2068            ExchangeEvents::TriggerDescIdTooLow(_) => vec![],
2069            ExchangeEvents::TriggerOrderExecution(_) => vec![],
2070            ExchangeEvents::TriggerOrderRequest(_) => vec![],
2071            ExchangeEvents::UnableToCancelOrder(_) => vec![],
2072            ExchangeEvents::UnityDescentThreshUpdated(_) => vec![],
2073            ExchangeEvents::UnspecifiedCollateral(_) => vec![],
2074            ExchangeEvents::UnwindCompleted(_) => vec![],
2075            ExchangeEvents::UnwindContractTrigger(_) => vec![],
2076            ExchangeEvents::UnwindInitializationCleared(_) => vec![],
2077            ExchangeEvents::UnwindInitialized(_) => vec![],
2078            ExchangeEvents::UnwindInsufficientBalance(_) => vec![],
2079            ExchangeEvents::UnwindIterationCompleted(_) => vec![],
2080            ExchangeEvents::UnwindPrepared(_) => vec![],
2081            ExchangeEvents::UnwindPreparationCleared(_) => vec![],
2082            ExchangeEvents::UnwindProcessInProgress(_) => vec![],
2083            ExchangeEvents::UpdateOracleFailed(_) => vec![],
2084            ExchangeEvents::Upgraded(_) => vec![],
2085            ExchangeEvents::ValueExceedsMaximum(_) => self
2086                .err_ctx(ctx, event)?
2087                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::ValueExceedsMaximum))
2088                .into_iter()
2089                .collect(),
2090            ExchangeEvents::WhitelistAddress(_) => vec![],
2091            ExchangeEvents::WhitelistingEnabledChanged(_) => vec![],
2092            ExchangeEvents::WithdrawRateLimitBypassSet(_) => vec![],
2093            ExchangeEvents::WithdrawRateLimitForceReset(_) => vec![],
2094            ExchangeEvents::WRLSMinWithdrawLimitUpdated(_) => vec![],
2095            ExchangeEvents::WRLSThousandthsTvlUpdated(_) => vec![],
2096            ExchangeEvents::WithdrawRateLimitReset(_) => vec![],
2097            ExchangeEvents::WrongAccountForOrder(_) => self
2098                .err_ctx(ctx, event)?
2099                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::WrongAccountForOrder))
2100                .into_iter()
2101                .collect(),
2102        })
2103    }
2104
2105    /// Starts tracking a newly listed perpetual contract, shared by the V1 and
2106    /// V2 listing events.
2107    fn add_perpetual(
2108        &mut self,
2109        instant: types::StateInstant,
2110        name: &str,
2111        symbol: &str,
2112        listing: RawContractAdded,
2113        fee_schedule: FeeSchedule,
2114    ) -> StateEvents {
2115        let perp = Perpetual::added(
2116            instant,
2117            listing.perp_id.to(),
2118            name.to_string(),
2119            symbol.to_string(),
2120            listing.status == 0, // PerpStatusEnum::Paused
2121            listing.price_decimals.to(),
2122            listing.lot_decimals.to(),
2123            listing.base_price_pns,
2124            fee_schedule,
2125            listing.init_margin_frac_hdths,
2126            listing.maint_margin_frac_hdths,
2127        );
2128        let event = StateEvents::perpetual(&perp, PerpetualEventType::Added);
2129        self.perpetuals.insert(perp.id(), perp);
2130        event
2131    }
2132
2133    /// Applies a maker fill, shared by the V1 and V2 event variants which
2134    /// differ only in carrying the builder fee earned on the fill.
2135    fn apply_maker_order_filled(
2136        &mut self,
2137        instant: types::StateInstant,
2138        event: &stream::RawEvent,
2139        ctx: &mut Option<OrderContext>,
2140        fill: RawMakerFill,
2141    ) -> Result<Vec<StateEvents>, DexError> {
2142        let cc = self.collateral_converter;
2143        Ok(chain!(
2144            if let Some((perp, order)) = self.order(fill.perp_id, fill.order_id)? {
2145                let fill_price = perp.price_converter().from_unsigned(fill.price_pns);
2146                let fill_size = perp.size_converter().from_unsigned(fill.lot_lns);
2147                let fee = cc.from_unsigned(fill.fee_cns);
2148                let builder_fee = cc.from_unsigned(fill.builder_fee_cns);
2149                perp.update_last_price(instant, fill_price);
2150                let clearing_remaining_order = if let Some(ctx) = ctx {
2151                    ctx.maker_fills.push(types::MakerFill {
2152                        log_index: event.log_index(),
2153                        maker_account_id: order.account_id(),
2154                        maker_order_id: order.order_id(),
2155                        maker_client_order_id: order.client_order_id(),
2156                        price: fill_price,
2157                        size: fill_size,
2158                        fee,
2159                        builder: order.builder(),
2160                        builder_fee,
2161                    });
2162                    let position_closed_by_smart_contract = if event.log_index() > 0 {
2163                        // Smart contract explicitly removes Close* order if position was
2164                        // closed, between `PositionClosed` and `MakerOrderFilled` events
2165                        // there can be a `RecycleFeeToAccount` event as well
2166                        match order.r#type() {
2167                            OrderType::CloseLong | OrderType::CloseShort => {
2168                                Some(event.log_index() - 1) == ctx.position_closed_at_log_index
2169                                    || Some(event.log_index() - 2)
2170                                        == ctx.position_closed_at_log_index
2171                            },
2172                            _ => false,
2173                        }
2174                    } else {
2175                        false
2176                    };
2177                    ctx.clearing_remaining_order | position_closed_by_smart_contract
2178                } else {
2179                    false
2180                };
2181                vec![
2182                    if order.size() > fill_size && !clearing_remaining_order {
2183                        let new_size = order.size() - fill_size;
2184                        perp.update_order(order.updated(
2185                            instant,
2186                            ctx,
2187                            None,
2188                            Some(new_size),
2189                            None,
2190                            None,
2191                        ))
2192                        .expect("order exists");
2193                        StateEvents::order(
2194                            perp,
2195                            &order,
2196                            ctx,
2197                            OrderEventType::Updated {
2198                                price: None,
2199                                size: Some(new_size),
2200                                expiry_block: None,
2201                            },
2202                        )
2203                    } else {
2204                        perp.remove_order(order.order_id()).expect("order exists");
2205                        StateEvents::order(perp, &order, ctx, OrderEventType::Removed)
2206                    },
2207                    StateEvents::order(
2208                        perp,
2209                        &order,
2210                        ctx,
2211                        OrderEventType::Filled {
2212                            fill_price,
2213                            fill_size,
2214                            fee,
2215                            builder_fee,
2216                            is_maker: true,
2217                        },
2218                    ),
2219                    StateEvents::perpetual(
2220                        perp,
2221                        PerpetualEventType::LastPriceUpdated(perp.last_price()),
2222                    ),
2223                ]
2224            } else {
2225                vec![]
2226            },
2227            self.account(fill.account_id).map(|acc| {
2228                acc.update_locked_balance(instant, cc.from_unsigned(fill.locked_balance_cns));
2229                StateEvents::account(
2230                    acc,
2231                    ctx,
2232                    AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
2233                )
2234            }),
2235            self.account(fill.account_id).map(|acc| {
2236                acc.update_balance(instant, cc.from_unsigned(fill.balance_cns));
2237                StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
2238            }),
2239        )
2240        .collect())
2241    }
2242
2243    /// Applies a taker fill, shared by the V1 and V2 event variants which
2244    /// differ only in carrying the builder fee earned on the fill.
2245    fn apply_taker_order_filled(
2246        &mut self,
2247        instant: types::StateInstant,
2248        event: &stream::RawEvent,
2249        ctx: &mut Option<OrderContext>,
2250        fill: RawTakerFill,
2251    ) -> Result<Vec<StateEvents>, DexError> {
2252        let cc = self.collateral_converter;
2253        let c = ctx
2254            .as_ref()
2255            .ok_or(DexError::OrderContextExpected(event.tx_index(), event.log_index()))?;
2256        let taker_fee = cc.from_unsigned(fill.fee_cns);
2257        let taker_builder_fee = cc.from_unsigned(fill.builder_fee_cns);
2258        Ok(chain!(
2259            self.perpetuals
2260                .get(&c.perpetual_id)
2261                .map(|perp| StateEvents::Order(OrderEvent {
2262                    perpetual_id: perp.id(),
2263                    account_id: c.account_id,
2264                    request_id: Some(c.request_id),
2265                    client_order_id: Some(c.request_id),
2266                    order_id: None,
2267                    builder: c.builder,
2268                    r#type: OrderEventType::Filled {
2269                        fill_price: perp.price_converter().from_unsigned(fill.collat_price_pns),
2270                        fill_size: perp.size_converter().from_unsigned(fill.lot_lns),
2271                        fee: taker_fee,
2272                        builder_fee: taker_builder_fee,
2273                        is_maker: false,
2274                    },
2275                })),
2276            self.accounts.get_mut(&c.account_id).map(|acc| {
2277                acc.update_balance(instant, cc.from_unsigned(fill.balance_cns));
2278                StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
2279            }),
2280            iter::once(StateEvents::trade(c, taker_fee, taker_builder_fee)),
2281        )
2282        .collect())
2283    }
2284
2285    /// Registers a rewrite of a fee schedule, fanning it out to every tracked
2286    /// contract *currently pointing at it* - a contract keyed by the same id
2287    /// but resolving its fees elsewhere is left alone, as only
2288    /// `PerpFeeSchedIdSet` moves a contract between schedules.
2289    fn update_fee_schedule(
2290        &mut self,
2291        instant: types::StateInstant,
2292        schedule: FeeSchedule,
2293    ) -> Vec<StateEvents> {
2294        self.fee_schedules.set(schedule);
2295        chain!(
2296            iter::once(StateEvents::Exchange(ExchangeEvent::FeeScheduleUpdated(schedule))),
2297            self.perpetuals
2298                .values_mut()
2299                .filter(|perp| perp.fee_schedule().key() == schedule.key())
2300                .map(|perp| {
2301                    perp.update_fee_schedule(instant, schedule);
2302                    StateEvents::perpetual(
2303                        perp,
2304                        PerpetualEventType::FeeScheduleUpdated(perp.fee_schedule()),
2305                    )
2306                }),
2307        )
2308        .collect()
2309    }
2310
2311    fn apply_state_event(
2312        &mut self,
2313        instant: types::StateInstant,
2314        event: &StateEvents,
2315    ) -> Result<Vec<StateEvents>, DexError> {
2316        Ok(match event {
2317            StateEvents::Perpetual(pe) => {
2318                match pe.r#type {
2319                    PerpetualEventType::MaintenanceMarginFractionUpdated(maintenance_margin) => {
2320                        // Applying new maintenance margin to all tracked positions
2321                        self.accounts
2322                            .values_mut()
2323                            .filter_map(|acc| {
2324                                acc.positions_mut().get_mut(&pe.perpetual_id).map(|pos| {
2325                                    pos.apply_maintenance_margin(instant, maintenance_margin);
2326                                    StateEvents::position(
2327                                        pos,
2328                                        &None,
2329                                        PositionEventType::MaintenanceMarginUpdated(
2330                                            pos.maintenance_margin_requirement(),
2331                                        ),
2332                                    )
2333                                })
2334                            })
2335                            .collect()
2336                    },
2337                    _ => vec![],
2338                }
2339            },
2340            _ => vec![],
2341        })
2342    }
2343
2344    fn err_ctx<'c>(
2345        &self,
2346        ctx: &'c mut Option<OrderContext>,
2347        event: &stream::RawEvent,
2348    ) -> Result<Option<&'c OrderContext>, DexError> {
2349        let c = ctx
2350            .as_ref()
2351            .ok_or(DexError::OrderContextExpected(event.tx_index(), event.log_index()))?;
2352        Ok(self.accounts.contains_key(&c.account_id).then_some(c))
2353    }
2354
2355    fn ensure_account(&mut self, id: U256) {
2356        let id = id.to::<types::AccountId>();
2357        if self.track_all_accounts && !self.accounts.contains_key(&id) {
2358            self.accounts
2359                .insert(id, Account::untracked(types::StateInstant::default(), id));
2360        }
2361    }
2362
2363    fn account(&mut self, id: U256) -> Option<&mut Account> {
2364        self.ensure_account(id);
2365        self.accounts.get_mut(&id.to::<types::AccountId>())
2366    }
2367
2368    fn order(
2369        &mut self,
2370        perp_id: U256,
2371        ord_id: U256,
2372    ) -> Result<Option<(&mut Perpetual, Order)>, DexError> {
2373        let ord_id = std::num::NonZeroU16::new(ord_id.to::<u16>())
2374            .expect("ord_id in order lookup cannot be 0");
2375        Ok(if let Some(perp) = self.perpetuals.get_mut(&perp_id.to::<types::PerpetualId>()) {
2376            let ord = perp
2377                .get_order(ord_id)
2378                .copied()
2379                .ok_or(DexError::OrderNotFound(perp.id(), ord_id))?;
2380            Some((perp, ord))
2381        } else {
2382            None
2383        })
2384    }
2385
2386    fn perpetual(&mut self, id: U256) -> Option<&mut Perpetual> {
2387        self.perpetuals.get_mut(&id.to::<types::PerpetualId>())
2388    }
2389
2390    fn account_perpetual(
2391        &mut self,
2392        acc_id: U256,
2393        perp_id: U256,
2394    ) -> Option<(&mut Account, &mut Perpetual)> {
2395        self.ensure_account(acc_id);
2396        self.accounts
2397            .get_mut(&acc_id.to::<types::AccountId>())
2398            .zip(self.perpetuals.get_mut(&perp_id.to::<types::PerpetualId>()))
2399    }
2400
2401    fn position(
2402        &mut self,
2403        acc_id: U256,
2404        perp_id: U256,
2405    ) -> Result<Option<(&mut Position, &mut Perpetual)>, DexError> {
2406        self.ensure_account(acc_id);
2407        let acc_id = acc_id.to::<types::AccountId>();
2408        let perp_id = perp_id.to::<types::PerpetualId>();
2409        Ok(
2410            if let Some(acc) = self.accounts.get_mut(&acc_id)
2411                && let Some(perp) = self.perpetuals.get_mut(&perp_id)
2412            {
2413                let pos = acc
2414                    .positions_mut()
2415                    .get_mut(&perp_id)
2416                    .ok_or(DexError::PositionNotFound(acc_id, perp_id))?;
2417                Some((pos, perp))
2418            } else {
2419                None
2420            },
2421        )
2422    }
2423}
2424
2425#[cfg(feature = "display")]
2426impl std::fmt::Display for Exchange {
2427    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2428        use colored::Colorize;
2429        use tabled::{Table, settings::Style};
2430
2431        writeln!(
2432            f,
2433            "{}",
2434            format!(
2435                "{} | {}{} | {} (sdk {}) | chain {}",
2436                self.instant,
2437                if self.is_halted { "[HALTED] ".bold().bright_red() } else { Default::default() },
2438                self.chain.exchange(),
2439                self.features,
2440                Self::revision(),
2441                self.chain.chain_id(),
2442            )
2443            .bold()
2444            .purple(),
2445        )?;
2446
2447        let mut params = Table::from_iter(vec![vec![
2448            format!("Min Post: {}", self.min_post),
2449            format!("Min Settle: {}", self.min_settle),
2450            format!("Recycle Fee: {}", self.recycle_fee),
2451            format!("Funding Interval: {}", self.funding_interval_blocks),
2452        ]]);
2453        params.with(Style::modern());
2454        writeln!(f, "{params}")?;
2455
2456        // One row per registered schedule, the tier rates stacked taker over
2457        // maker. Pre-v1.1.7.4 contracts have no schedule registry - fees live on
2458        // the perpetual contract itself, and are rendered with it.
2459        //
2460        // Full renderings only: the whole tier grid is a dozen rarely-changing
2461        // rates, too much of a header for a live view that redraws every block.
2462        // Each perpetual still reports the base rates it resolves to.
2463        if f.alternate() && self.features.keyed_fee_schedules() {
2464            let mut fees = Table::from_iter(chain!(
2465                iter::once(
2466                    chain!(
2467                        iter::once("Fees (tkr/mkr)".to_string()),
2468                        (0..FEE_TIERS).map(|tier| format!("Tier {tier}")),
2469                    )
2470                    .collect::<Vec<_>>(),
2471                ),
2472                self.fee_schedules.schedules().map(|schedule| chain!(
2473                    iter::once(schedule.key().to_string()),
2474                    (0..FEE_TIERS as types::FeeTier).map(move |tier| format!(
2475                        "{}\n{}",
2476                        schedule.taker_fee(tier),
2477                        schedule.maker_fee(tier),
2478                    )),
2479                )
2480                .collect::<Vec<_>>()),
2481            ));
2482            fees.with(Style::modern());
2483            writeln!(f, "{fees}")?;
2484        }
2485        writeln!(f)?;
2486
2487        // Render perpetuals and accounts in alternate mode
2488        if f.alternate() && !self.perpetuals().is_empty() {
2489            let mut perpetuals: Vec<_> = self.perpetuals().values().collect();
2490            perpetuals.sort_by_key(|p| p.id());
2491            for perpetual in perpetuals {
2492                writeln!(f, "{:#}", perpetual)?;
2493            }
2494        }
2495
2496        if f.alternate() && !self.accounts().is_empty() {
2497            let mut accounts: Vec<_> = self.accounts().values().collect();
2498            accounts.sort_by_key(|a| a.id());
2499            for account in accounts {
2500                writeln!(f, "{:#}", account)?;
2501            }
2502        }
2503
2504        Ok(())
2505    }
2506}