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                let fee_converter = num::fee_converter();
620                vec![self.add_perpetual(
621                    instant,
622                    &e.name,
623                    &e.symbol,
624                    RawContractAdded {
625                        perp_id: e.perpId,
626                        status: e.status,
627                        price_decimals: e.priceDecimals,
628                        lot_decimals: e.lotDecimals,
629                        base_price_pns: e.basePricePNS,
630                        init_margin_frac_hdths: e.initMarginFracHdths,
631                        maint_margin_frac_hdths: e.maintMarginFracHdths,
632                    },
633                    FeeSchedule::flat(
634                        FeeScheduleKey::Default,
635                        fee_converter.from_unsigned(e.takerFeePer100K),
636                        fee_converter.from_unsigned(e.makerFeePer100K),
637                    ),
638                )]
639            },
640            ExchangeEvents::ContractAddedV2(e) => {
641                // The listing reports the contract's fee schedule KEY, the rates
642                // being resolvable from it - a new contract is placed on the
643                // exchange-wide default schedule, and no `PerpFeeSchedIdSet`
644                // accompanies the listing to report the id separately.
645                let key = FeeScheduleKey::from_raw(e.perpFeeSchedId);
646                let fee_schedule = self.fee_schedule(key).ok_or(
647                    // A custom schedule of a contract that does not exist yet has
648                    // no rates to resolve; the contract documents that a listing
649                    // is always placed on a shared schedule, so this is
650                    // unreachable in practice
651                    DexError::FeeScheduleNotFound(key),
652                )?;
653                vec![self.add_perpetual(
654                    instant,
655                    &e.name,
656                    &e.symbol,
657                    RawContractAdded {
658                        perp_id: e.perpId,
659                        status: e.status,
660                        price_decimals: e.priceDecimals,
661                        lot_decimals: e.lotDecimals,
662                        base_price_pns: e.basePricePNS,
663                        init_margin_frac_hdths: e.initMarginFracHdths,
664                        maint_margin_frac_hdths: e.maintMarginFracHdths,
665                    },
666                    fee_schedule,
667                )]
668            },
669            ExchangeEvents::ContractNotOperational(_) => self
670                .err_ctx(ctx, event)?
671                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::ContractNotOperational))
672                .into_iter()
673                .collect(),
674            ExchangeEvents::ContractLinkFeedUpdated(e) => self
675                .perpetual(e.perpId)
676                .map(|perp| {
677                    perp.update_oracle_feed_id(instant, e.feedId);
678                    StateEvents::perpetual(
679                        perp,
680                        PerpetualEventType::OracleConfigurationUpdated {
681                            is_used: perp.is_oracle_used(),
682                            feed_id: perp.oracle_feed_id(),
683                        },
684                    )
685                })
686                .into_iter()
687                .collect(),
688            ExchangeEvents::ContractPaused(e) => {
689                // Bound before the `&mut self` borrow below; a first unpause
690                // anchors the contract's funding schedule to this grid.
691                let funding_interval = self.funding_interval_blocks as u64;
692                self.perpetual(e.perpId)
693                    .map(|perp| {
694                        perp.update_paused(instant, e.paused, funding_interval);
695                        StateEvents::perpetual(perp, PerpetualEventType::Paused(perp.is_paused()))
696                    })
697                    .into_iter()
698                    .collect()
699            },
700            ExchangeEvents::ContractRemoved(e) => {
701                let funding_interval = self.funding_interval_blocks as u64;
702                self.perpetual(e.perpId)
703                    .map(|perp| {
704                        perp.update_paused(instant, true, funding_interval);
705                        StateEvents::perpetual(perp, PerpetualEventType::Paused(perp.is_paused()))
706                    })
707                    .into_iter()
708                    .collect()
709            },
710            ExchangeEvents::ContractVersionSet(e) => {
711                let version = ContractVersion::new(e.major.to(), e.minor.to(), e.patch.to());
712                self.features.observe_version(version);
713                vec![StateEvents::Exchange(ExchangeEvent::ContractVersionUpdated(version))]
714            },
715            ExchangeEvents::CrossesBook(_) => self
716                .err_ctx(ctx, event)?
717                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::CrossesBook))
718                .into_iter()
719                .collect(),
720            ExchangeEvents::DcpBorrowThreshUpdated(_) => vec![],
721            ExchangeEvents::DecreaseCollateralBeyondMarkPrice(_) => vec![],
722            ExchangeEvents::DefaultPerpFeeScheduleSet(e) => self.update_fee_schedule(
723                instant,
724                FeeSchedule::new(
725                    FeeScheduleKey::Default,
726                    e.takerFeesPer100K,
727                    e.makerFeesPer100K,
728                    num::fee_converter(),
729                ),
730            ),
731            ExchangeEvents::DefaultRwaFeeScheduleSet(e) => self.update_fee_schedule(
732                instant,
733                FeeSchedule::new(
734                    FeeScheduleKey::RwaDefault,
735                    e.takerFeesPer100K,
736                    e.makerFeesPer100K,
737                    num::fee_converter(),
738                ),
739            ),
740            ExchangeEvents::DeleveragePositionListEmpty(_) => vec![],
741            ExchangeEvents::ExceedsLastExecutionBlock(_) => self
742                .err_ctx(ctx, event)?
743                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::ExceedsLastExecutionBlock))
744                .into_iter()
745                .collect(),
746            ExchangeEvents::ExchangeHalted(e) => {
747                self.is_halted = e.halted;
748                vec![StateEvents::Exchange(ExchangeEvent::Halted(self.is_halted))]
749            },
750            ExchangeEvents::ExchangeInitialized(_) => vec![],
751            ExchangeEvents::FeeParamsUpdated(_) => vec![],
752            ExchangeEvents::FeeScheduleSet(e) => {
753                // `setFeeSchedValues(id, …)` rewrites the rates of the schedule
754                // registered under `id`, whichever contracts happen to point at
755                // it. It does NOT repoint anything: a custom id names a schedule
756                // *keyed by* a perpetual's id, not that perpetual's current
757                // schedule - only `PerpFeeSchedIdSet` moves a contract onto it.
758                self.update_fee_schedule(
759                    instant,
760                    FeeSchedule::new(
761                        FeeScheduleKey::from_raw(e.feeSchedId),
762                        e.takerFeesPer100K,
763                        e.makerFeesPer100K,
764                        num::fee_converter(),
765                    ),
766                )
767            },
768            ExchangeEvents::FundingClampPctUpdated(_) => vec![],
769            ExchangeEvents::FundingEventCompleted(e) => {
770                if let Some(perp) = self.perpetual(e.perpId) {
771                    perp.update_funding(
772                        instant,
773                        perp.funding_rate_converter()
774                            .from_signed(e.actualRatePct100k),
775                        perp.funding_sum_converter()
776                            .from_i64(e.fundingPaymentPNS.as_i64()),
777                        e.fundingEventBlock.to(),
778                    );
779                }
780                vec![]
781            },
782            ExchangeEvents::FundingEventSetTooEarly(_) => vec![],
783            ExchangeEvents::FundingPriceExceedsTol(_) => vec![],
784            ExchangeEvents::FundingSumAlreadySet(_) => vec![],
785            ExchangeEvents::FundingSumScalingExpUpdated(e) => self
786                .perpetual(e.perpId)
787                .map(|perp| {
788                    perp.update_funding_sum_scaling_exp(instant, e.newExp.to());
789                    StateEvents::perpetual(
790                        perp,
791                        PerpetualEventType::FundingSumScalingExpUpdated(e.newExp.to()),
792                    )
793                })
794                .into_iter()
795                .collect(),
796            ExchangeEvents::IgnoreOracleUpdated(e) => self
797                .perpetual(e.perpId)
798                .map(|perp| {
799                    perp.update_is_oracle_used(instant, !e.ignOracle);
800                    StateEvents::perpetual(
801                        perp,
802                        PerpetualEventType::OracleConfigurationUpdated {
803                            is_used: perp.is_oracle_used(),
804                            feed_id: perp.oracle_feed_id(),
805                        },
806                    )
807                })
808                .into_iter()
809                .collect(),
810            ExchangeEvents::ImmediateOrCancelExecuted(_) => self
811                .err_ctx(ctx, event)?
812                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::ImmediateOrCancelExecuted))
813                .into_iter()
814                .collect(),
815            ExchangeEvents::IncreasePositionCollateral(e) => chain!(
816                self.position(e.accountId, e.perpId)?.map(|(pos, _)| {
817                    pos.update_deposit(instant, cc.from_unsigned(e.positionDepositCNS));
818                    StateEvents::position(
819                        pos,
820                        ctx,
821                        PositionEventType::DepositUpdated(pos.deposit()),
822                    )
823                }),
824                self.account(e.accountId).map(|acc| {
825                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
826                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
827                }),
828            )
829            .collect(),
830            ExchangeEvents::Initialized(_) => vec![],
831            ExchangeEvents::InitialMarginFractionUpdated(e) => self
832                .perpetual(e.perpId)
833                .map(|perp| {
834                    perp.update_initial_margin(
835                        instant,
836                        perp.leverage_converter()
837                            .from_unsigned(e.initMarginFracHdths),
838                    );
839                    StateEvents::perpetual(
840                        perp,
841                        PerpetualEventType::InitialMarginFractionUpdated(perp.initial_margin()),
842                    )
843                })
844                .into_iter()
845                .collect(),
846            ExchangeEvents::InsolventPositionCannotBeForcedClose(_) => vec![],
847            ExchangeEvents::InsuficientFundsForRecycleFee(_) => self
848                .err_ctx(ctx, event)?
849                .map(|ctx| {
850                    StateEvents::order_error(ctx, OrderErrorType::InsuficientFundsForRecycleFee)
851                })
852                .into_iter()
853                .collect(),
854            ExchangeEvents::InsufficientFundsToDecCollateral(_) => vec![],
855            ExchangeEvents::InsurancePaymentForSettlement(_) => vec![],
856            ExchangeEvents::InvalidAccountFrozenOrder(_) => vec![],
857            ExchangeEvents::InvalidBankruptcyPrice(_) => vec![],
858            ExchangeEvents::InvalidExpiryBlock(_) => self
859                .err_ctx(ctx, event)?
860                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::InvalidExpiryBlock))
861                .into_iter()
862                .collect(),
863            ExchangeEvents::InvalidLinkReportForContract(_) => vec![],
864            ExchangeEvents::InvalidLinkReportVersion(_) => vec![],
865            ExchangeEvents::InvalidLiquidationPrice(_) => vec![],
866            ExchangeEvents::InvalidOrderId(_) => self
867                .err_ctx(ctx, event)?
868                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::InvalidOrderId))
869                .into_iter()
870                .collect(),
871            ExchangeEvents::LastForwardedDescIdReset(_) => vec![],
872            ExchangeEvents::LastTriggeredDescIdReset(_) => vec![],
873            ExchangeEvents::LinkDatastreamConfigured(_) => vec![],
874            ExchangeEvents::LinkDsError_0(_) => vec![],
875            ExchangeEvents::LinkDsError_1(_) => vec![],
876            ExchangeEvents::LinkDsPanic(_) => vec![],
877            ExchangeEvents::LinkPriceUpdated(e) => self
878                .perpetual(e.perpId)
879                .map(|perp| {
880                    perp.update_oracle_price(
881                        instant,
882                        perp.price_converter().from_unsigned(e.oraclePricePNS),
883                    );
884                    StateEvents::perpetual(
885                        perp,
886                        PerpetualEventType::OraclePriceUpdated(perp.oracle_price()),
887                    )
888                })
889                .into_iter()
890                .collect(),
891            ExchangeEvents::LiquidationBuyerUpdated(_) => vec![],
892            ExchangeEvents::LiquidationParamsUpdated(_) => vec![],
893            ExchangeEvents::LotOutOfRange(_) => self
894                .err_ctx(ctx, event)?
895                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::SizeOutOfRange))
896                .into_iter()
897                .collect(),
898            ExchangeEvents::MaintenanceMarginFractionUpdated(e) => self
899                .perpetual(e.perpId)
900                .map(|perp| {
901                    perp.update_maintenance_margin(
902                        instant,
903                        perp.leverage_converter()
904                            .from_unsigned(e.maintMarginFracHdths),
905                    );
906                    StateEvents::perpetual(
907                        perp,
908                        PerpetualEventType::MaintenanceMarginFractionUpdated(
909                            perp.maintenance_margin(),
910                        ),
911                    )
912                })
913                .into_iter()
914                .collect(),
915            // Deprecated in v1.1.7.4, replayed from earlier history only
916            ExchangeEvents::MakerFeeUpdated(e) => self
917                .perpetual(e.perpId)
918                .map(|perp| {
919                    perp.update_base_maker_fee(
920                        instant,
921                        perp.fee_converter().from_unsigned(e.makerFeePer100K),
922                    );
923                    StateEvents::perpetual(
924                        perp,
925                        PerpetualEventType::MakerFeeUpdated(perp.maker_fee()),
926                    )
927                })
928                .into_iter()
929                .collect(),
930            // Superseded by `MakerOrderFilledV2` in v1.1.7.4, replayed from
931            // earlier history only, hence no builder fee
932            ExchangeEvents::MakerOrderFilled(e) => self.apply_maker_order_filled(
933                instant,
934                event,
935                ctx,
936                RawMakerFill {
937                    perp_id: e.perpId,
938                    account_id: e.accountId,
939                    order_id: e.orderId,
940                    price_pns: e.pricePNS,
941                    lot_lns: e.lotLNS,
942                    fee_cns: e.feeCNS,
943                    builder_fee_cns: U256::ZERO,
944                    locked_balance_cns: e.lockedBalanceCNS,
945                    balance_cns: e.balanceCNS,
946                },
947            )?,
948            ExchangeEvents::MakerOrderFilledV2(e) => self.apply_maker_order_filled(
949                instant,
950                event,
951                ctx,
952                RawMakerFill {
953                    perp_id: e.perpId,
954                    account_id: e.accountId,
955                    order_id: e.orderId,
956                    price_pns: e.pricePNS,
957                    lot_lns: e.lotLNS,
958                    fee_cns: e.feeCNS,
959                    builder_fee_cns: e.builderFeeCNS,
960                    locked_balance_cns: e.lockedBalanceCNS,
961                    balance_cns: e.balanceCNS,
962                },
963            )?,
964            ExchangeEvents::MakerOrderSettlementFailed(e) => chain!(
965                if let Some(perp) = self.perpetual(e.perpId) {
966                    let order_id = std::num::NonZeroU16::new(e.orderId.to::<u16>())
967                        .expect("orderId in event cannot be 0");
968                    let order = perp.remove_order(order_id)?;
969                    chain!(
970                        Some(StateEvents::order(perp, &order, ctx, OrderEventType::Removed)),
971                        self.err_ctx(ctx, event)?
972                            .map(|ctx| StateEvents::affected_order_error(
973                                ctx,
974                                &order,
975                                OrderErrorType::MakerOrderSettlementFailed
976                            ))
977                    )
978                    .collect()
979                } else {
980                    vec![]
981                },
982                self.account(e.accountId).map(|acc| {
983                    acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
984                    StateEvents::account(
985                        acc,
986                        ctx,
987                        AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
988                    )
989                }),
990                if !e.recyclerAmountCNS.is_zero() {
991                    self.account(e.recyclerAccountId).map(|acc| {
992                        acc.update_balance(instant, cc.from_unsigned(e.recyclerBalanceCNS));
993                        StateEvents::account(
994                            acc,
995                            ctx,
996                            AccountEventType::BalanceUpdated(acc.balance()),
997                        )
998                    })
999                } else {
1000                    None
1001                },
1002            )
1003            .collect(),
1004            ExchangeEvents::MarginTolUpdated(_) => vec![],
1005            ExchangeEvents::MarkExceedsTol(_) => vec![],
1006            ExchangeEvents::MarkPriceAgeExceedsMax(_) => vec![],
1007            ExchangeEvents::MarkUpdated(e) => {
1008                let perp_mark = self.perpetual(e.perpId).map(|perp| {
1009                    perp.update_mark_price(
1010                        instant,
1011                        perp.price_converter().from_unsigned(e.pricePNS),
1012                    );
1013                    (perp.id(), perp.mark_price())
1014                });
1015                if let Some((perp_id, mark_price)) = perp_mark {
1016                    chain!(
1017                        Some(StateEvents::Perpetual(PerpetualEvent {
1018                            perpetual_id: perp_id,
1019                            r#type: PerpetualEventType::MarkPriceUpdated(mark_price),
1020                        })),
1021                        // Applying updated mark to all tracked positions
1022                        self.accounts.values_mut().filter_map(|acc| {
1023                            acc.positions_mut().get_mut(&perp_id).map(|pos| {
1024                                pos.apply_mark_price(instant, mark_price);
1025                                StateEvents::position(
1026                                    pos,
1027                                    &None,
1028                                    PositionEventType::UnrealizedPnLUpdated {
1029                                        pnl: pos.pnl(),
1030                                        delta_pnl: pos.delta_pnl(),
1031                                        premium_pnl: pos.premium_pnl(),
1032                                    },
1033                                )
1034                            })
1035                        }),
1036                    )
1037                    .collect()
1038                } else {
1039                    vec![]
1040                }
1041            },
1042            ExchangeEvents::MaxMatchesReached(_) => self
1043                .err_ctx(ctx, event)?
1044                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::MaxMatchesReached))
1045                .into_iter()
1046                .collect(),
1047            ExchangeEvents::MaxOpenInterestUpdated(_) => vec![],
1048            ExchangeEvents::MaximumAccountOrders(_) => self
1049                .err_ctx(ctx, event)?
1050                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::MaximumAccountOrders))
1051                .into_iter()
1052                .collect(),
1053            ExchangeEvents::MinAccountOpenAmountUpdated(_) => vec![],
1054            ExchangeEvents::MinPostUpdated(e) => {
1055                self.min_post = cc.from_unsigned(e.minPostCNS);
1056                vec![StateEvents::Exchange(ExchangeEvent::MinPostUpdated(self.min_post))]
1057            },
1058            ExchangeEvents::MinSettleUpdated(e) => {
1059                self.min_settle = cc.from_unsigned(e.minSettleCNS);
1060                vec![StateEvents::Exchange(ExchangeEvent::MinSettleUpdated(self.min_settle))]
1061            },
1062            ExchangeEvents::MonitorAdministratorUpdated(_) => vec![],
1063            ExchangeEvents::MonitorPauseAttempted(_) => vec![],
1064            ExchangeEvents::OracleAgeExceedsMax(_) => vec![],
1065            ExchangeEvents::OracleDisabled(_) => vec![],
1066            ExchangeEvents::OrderBatchCompleted(_) => {
1067                // Reset context
1068                ctx.take();
1069                vec![]
1070            },
1071            ExchangeEvents::OrderCancelled(e) => {
1072                let c = must_ctx()?;
1073                let order_id = c.order_id.expect("order_id required for OrderCancelled");
1074                chain!(
1075                    if let Some(perp) = self.perpetuals.get_mut(&c.perpetual_id) {
1076                        let order = perp.remove_order(order_id)?;
1077                        Some(StateEvents::order(perp, &order, ctx, OrderEventType::Removed))
1078                    } else {
1079                        None
1080                    },
1081                    if let Some(acc) = self.accounts.get_mut(&c.account_id) {
1082                        acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
1083                        acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
1084                        vec![
1085                            StateEvents::account(
1086                                acc,
1087                                ctx,
1088                                AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
1089                            ),
1090                            StateEvents::account(
1091                                acc,
1092                                ctx,
1093                                AccountEventType::BalanceUpdated(acc.balance()),
1094                            ),
1095                        ]
1096                    } else {
1097                        vec![]
1098                    },
1099                )
1100                .collect()
1101            },
1102            ExchangeEvents::OrderCancelledByAdmin(e) => chain!(
1103                self.order(e.perpId, e.orderId)?.map(|(perp, order)| {
1104                    perp.remove_order(order.order_id()).expect("order exists");
1105                    StateEvents::order(perp, &order, ctx, OrderEventType::Removed)
1106                }),
1107                self.account(e.accountId).map(|acc| {
1108                    acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
1109                    StateEvents::account(
1110                        acc,
1111                        ctx,
1112                        AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
1113                    )
1114                }),
1115            )
1116            .collect(),
1117            ExchangeEvents::OrderCancelledByLiquidator(e) => chain!(
1118                self.order(e.perpId, e.orderId)?.map(|(perp, order)| {
1119                    perp.remove_order(order.order_id()).expect("order exists");
1120                    StateEvents::order(perp, &order, ctx, OrderEventType::Removed)
1121                }),
1122                self.account(e.accountId).map(|acc| {
1123                    acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
1124                    StateEvents::account(
1125                        acc,
1126                        ctx,
1127                        AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
1128                    )
1129                }),
1130            )
1131            .collect(),
1132            ExchangeEvents::OrderChanged(e) => {
1133                let c = must_ctx()?;
1134                let order_id = c.order_id.expect("order_id required for OrderChanged");
1135                chain!(
1136                    if let Some(perp) = self.perpetuals.get_mut(&c.perpetual_id) {
1137                        let order = perp
1138                            .get_order(order_id)
1139                            .copied()
1140                            .ok_or(DexError::OrderNotFound(perp.id(), order_id))?;
1141                        let new_price = perp.price_converter().from_unsigned(e.pricePNS);
1142                        let new_size = perp.size_converter().from_unsigned(e.lotLNS);
1143                        let new_expiry_block = e.expiryBlock.to();
1144                        let price_update =
1145                            if order.price() != new_price { Some(new_price) } else { None };
1146                        let size_update =
1147                            if order.size() != new_size { Some(new_size) } else { None };
1148                        let expiry_block_update = if order.expiry_block() != new_expiry_block {
1149                            Some(new_expiry_block)
1150                        } else {
1151                            None
1152                        };
1153                        let updated = order.updated(
1154                            instant,
1155                            ctx,
1156                            price_update,
1157                            size_update,
1158                            size_update,
1159                            expiry_block_update,
1160                        );
1161                        perp.update_order(updated)?;
1162                        Some(StateEvents::order(
1163                            perp,
1164                            &order,
1165                            ctx,
1166                            OrderEventType::Updated {
1167                                price: price_update,
1168                                size: size_update,
1169                                expiry_block: expiry_block_update,
1170                            },
1171                        ))
1172                    } else {
1173                        None
1174                    },
1175                    if let Some(acc) = self.accounts.get_mut(&c.account_id) {
1176                        acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
1177                        acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
1178                        vec![
1179                            StateEvents::account(
1180                                acc,
1181                                ctx,
1182                                AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
1183                            ),
1184                            StateEvents::account(
1185                                acc,
1186                                ctx,
1187                                AccountEventType::BalanceUpdated(acc.balance()),
1188                            ),
1189                        ]
1190                    } else {
1191                        vec![]
1192                    },
1193                )
1194                .collect()
1195            },
1196            ExchangeEvents::OrderDescIdTooLow(_) => vec![],
1197            ExchangeEvents::OrderDoesNotExist(_) => self
1198                .err_ctx(ctx, event)?
1199                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::OrderDoesNotExist))
1200                .into_iter()
1201                .collect(),
1202            ExchangeEvents::OrderExtensionRejected(_) => self
1203                .err_ctx(ctx, event)?
1204                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::OrderExtensionRejected))
1205                .into_iter()
1206                .collect(),
1207            ExchangeEvents::OrderForwardingNotAllowed(_) => vec![],
1208            ExchangeEvents::OrderForwardingUpdated(_) => vec![],
1209            ExchangeEvents::OrderPlaced(e) => {
1210                let c = must_ctx()?;
1211                let order_id = std::num::NonZeroU16::new(e.orderId.to::<u16>())
1212                    .expect("orderId in OrderPlaced event cannot be 0");
1213                chain!(
1214                    if let Some(perp) = self.perpetuals.get_mut(&c.perpetual_id) {
1215                        let order = Order::placed(
1216                            instant,
1217                            c,
1218                            order_id,
1219                            perp.size_converter().from_unsigned(e.lotLNS),
1220                            perp.price_converter(),
1221                            perp.leverage_converter(),
1222                        );
1223                        let event = OrderEventType::Placed {
1224                            r#type: order.r#type(),
1225                            price: order.price(),
1226                            size: order.size(),
1227                            expiry_block: order.expiry_block(),
1228                            leverage: order.leverage(),
1229                            post_only: order.post_only().unwrap_or_default(),
1230                            fill_or_kill: order.fill_or_kill().unwrap_or_default(),
1231                            immediate_or_cancel: order.immediate_or_cancel().unwrap_or_default(),
1232                        };
1233                        perp.add_order(order)?;
1234                        Some(StateEvents::order(perp, &order, ctx, event))
1235                    } else {
1236                        None
1237                    },
1238                    if let Some(acc) = self.accounts.get_mut(&c.account_id) {
1239                        acc.update_locked_balance(instant, cc.from_unsigned(e.lockedBalanceCNS));
1240                        acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
1241                        vec![
1242                            StateEvents::account(
1243                                acc,
1244                                ctx,
1245                                AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
1246                            ),
1247                            StateEvents::account(
1248                                acc,
1249                                ctx,
1250                                AccountEventType::BalanceUpdated(acc.balance()),
1251                            ),
1252                        ]
1253                    } else {
1254                        vec![]
1255                    },
1256                )
1257                .collect()
1258            },
1259            ExchangeEvents::OrderPostFailed(e) => self
1260                .err_ctx(ctx, event)?
1261                .map(|ctx| {
1262                    StateEvents::order_error(ctx, OrderErrorType::OrderPostFailed(e.reason.to()))
1263                })
1264                .into_iter()
1265                .collect(),
1266            // Superseded by `OrderRequestV2` in v1.1.7.4, replayed from earlier
1267            // history only
1268            ExchangeEvents::OrderRequest(e) => {
1269                // Store order request context as it is required to handle
1270                // future events
1271                ctx.replace(OrderContext::from(e));
1272                vec![]
1273            },
1274            ExchangeEvents::OrderRequestV2(e) => {
1275                ctx.replace(OrderContext::from(e));
1276                vec![]
1277            },
1278            ExchangeEvents::OrderSettlementImpliesInsolvent(_) => self
1279                .err_ctx(ctx, event)?
1280                .map(|ctx| {
1281                    StateEvents::order_error(ctx, OrderErrorType::OrderSettlementImpliesInsolvent)
1282                })
1283                .into_iter()
1284                .collect(),
1285            ExchangeEvents::OrderSizeExceedsAvailableSize(_) => self
1286                .err_ctx(ctx, event)?
1287                .map(|ctx| {
1288                    StateEvents::order_error(ctx, OrderErrorType::OrderSizeExceedsAvailableSize)
1289                })
1290                .into_iter()
1291                .collect(),
1292            ExchangeEvents::OverCollatDescentThreshUpdated(_) => vec![],
1293            ExchangeEvents::OwnershipTransferStarted(_) => vec![],
1294            ExchangeEvents::OwnershipTransferred(_) => vec![],
1295            ExchangeEvents::PermissonedCancelParamsUpdated(_) => vec![],
1296            ExchangeEvents::PerpFeeSchedIdSet(e) => {
1297                // The only event that repoints a contract at another schedule.
1298                // Id-only, so the rates come from the registry entry now pointed
1299                // at.
1300                //
1301                // A schedule never observed has no rates to resolve: the id is
1302                // applied on its own and the `FeeScheduleSet` that eventually
1303                // writes it reports the rates, rather than surfacing an
1304                // intermediate schedule pairing the new id with the old rates -
1305                // a state that never applies to a fill.
1306                let key = FeeScheduleKey::from_raw(e.feeSchedId);
1307                let schedule = self.fee_schedules.get(key);
1308                self.perpetual(e.perpId)
1309                    .and_then(|perp| match schedule {
1310                        Some(schedule) => {
1311                            perp.update_fee_schedule(instant, schedule);
1312                            Some(StateEvents::perpetual(
1313                                perp,
1314                                PerpetualEventType::FeeScheduleUpdated(perp.fee_schedule()),
1315                            ))
1316                        },
1317                        None => {
1318                            perp.update_fee_schedule_key(instant, key);
1319                            None
1320                        },
1321                    })
1322                    .into_iter()
1323                    .collect()
1324            },
1325            ExchangeEvents::PerpPositionBalCreditPositiveSevere(_) => vec![],
1326            ExchangeEvents::PositionAdministratorUpdated(_) => vec![],
1327            ExchangeEvents::PositionClosed(e) => {
1328                if let Some(ctx) = ctx {
1329                    ctx.position_closed_at_log_index = Some(event.log_index());
1330                }
1331
1332                if let Some((acc, perp)) = self.account_perpetual(e.accountId, e.perpId) {
1333                    let pos = acc
1334                        .positions_mut()
1335                        .remove(&perp.id())
1336                        .ok_or(DexError::PositionNotFound(acc.id(), perp.id()))?;
1337
1338                    chain!(
1339                        Some(StateEvents::position(
1340                            &pos,
1341                            ctx,
1342                            PositionEventType::Closed {
1343                                r#type: pos.r#type(),
1344                                entry_price: pos.entry_price(),
1345                                exit_price: perp.price_converter().from_unsigned(e.pricePNS),
1346                                size: pos.size(),
1347                                delta_pnl: cc.from_signed(e.deltaPnlCNS),
1348                                premium_pnl: cc.from_signed(e.fundingCNS),
1349                            }
1350                        )),
1351                        if PositionType::from(e.positionType) == PositionType::Long {
1352                            perp.update_open_interest(instant, pos.size(), UD64::ZERO);
1353                            Some(StateEvents::perpetual(
1354                                perp,
1355                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1356                            ))
1357                        } else {
1358                            None
1359                        },
1360                    )
1361                    .collect()
1362                } else {
1363                    vec![]
1364                }
1365            },
1366            ExchangeEvents::PositionCollateralDecreased(e) => {
1367                if let Some((pos, perp)) = self.position(e.accountId, e.perpId)? {
1368                    let prev_entry_price = pos.entry_price();
1369                    pos.update_entry_price(instant, e.endEntryPricePNS, 0, perp.price_converter());
1370                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1371                    pos.apply_mark_price(instant, perp.mark_price());
1372                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1373                    vec![StateEvents::position(
1374                        pos,
1375                        ctx,
1376                        PositionEventType::CollateralDecreased {
1377                            prev_entry_price,
1378                            new_entry_price: pos.entry_price(),
1379                            deposit: pos.deposit(),
1380                        },
1381                    )]
1382                } else {
1383                    vec![]
1384                }
1385            },
1386            ExchangeEvents::PositionDecreased(e) => {
1387                if let Some((pos, perp)) = self.position(e.accountId, e.perpId)? {
1388                    let prev_size = pos.size();
1389                    pos.update_size(instant, perp.size_converter().from_unsigned(e.endLotLNS));
1390                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1391                    pos.apply_mark_price(instant, perp.mark_price());
1392                    pos.update_premium_pnl(
1393                        instant,
1394                        pos.premium_pnl().sub(cc.from_signed(e.fundingCNS)),
1395                    );
1396                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1397                    chain!(
1398                        Some(StateEvents::position(
1399                            pos,
1400                            ctx,
1401                            PositionEventType::Decreased {
1402                                prev_size,
1403                                new_size: pos.size(),
1404                                deposit: pos.deposit(),
1405                                delta_pnl: pos.delta_pnl(),
1406                                premium_pnl: pos.premium_pnl(),
1407                            }
1408                        )),
1409                        if pos.r#type() == PositionType::Long {
1410                            perp.update_open_interest(instant, prev_size, pos.size());
1411                            Some(StateEvents::perpetual(
1412                                perp,
1413                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1414                            ))
1415                        } else {
1416                            None
1417                        },
1418                    )
1419                    .collect()
1420                } else {
1421                    vec![]
1422                }
1423            },
1424            ExchangeEvents::PositionDeleveraged(e) => chain!(
1425                if let Some((pos, perp)) = self.position(e.accountId, e.perpId)? {
1426                    let prev_size = pos.size();
1427                    pos.update_size(instant, perp.size_converter().from_unsigned(e.endLotLNS));
1428                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1429                    pos.apply_mark_price(instant, perp.mark_price());
1430                    pos.update_premium_pnl(
1431                        instant,
1432                        pos.premium_pnl().sub(cc.from_signed(e.fundingCNS)),
1433                    );
1434                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1435                    chain!(
1436                        Some(StateEvents::position(
1437                            pos,
1438                            ctx,
1439                            PositionEventType::Deleveraged {
1440                                force_close: e.forceClose,
1441                                r#type: pos.r#type(),
1442                                entry_price: pos.entry_price(),
1443                                exit_price: perp
1444                                    .price_converter()
1445                                    .from_unsigned(e.deleveragePricePNS),
1446                                prev_size,
1447                                new_size: pos.size(),
1448                                deposit: pos.deposit(),
1449                                delta_pnl: pos.delta_pnl(),
1450                                premium_pnl: pos.premium_pnl(),
1451                            }
1452                        )),
1453                        if pos.r#type() == PositionType::Long {
1454                            perp.update_open_interest(instant, prev_size, pos.size());
1455                            Some(StateEvents::perpetual(
1456                                perp,
1457                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1458                            ))
1459                        } else {
1460                            None
1461                        },
1462                    )
1463                    .collect()
1464                } else {
1465                    vec![]
1466                },
1467                self.account(e.accountId).map(|acc| {
1468                    if e.endLotLNS == U256::ZERO {
1469                        acc.positions_mut()
1470                            .remove(&e.perpId.to::<types::PerpetualId>());
1471                    }
1472                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
1473                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
1474                }),
1475            )
1476            .collect(),
1477            ExchangeEvents::PositionDeleveragedV2(e) => chain!(
1478                if let Some((pos, perp)) = self.position(e.accountId, e.perpId)? {
1479                    let prev_size = pos.size();
1480                    pos.update_size(instant, perp.size_converter().from_unsigned(e.endLotLNS));
1481                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1482                    pos.apply_mark_price(instant, perp.mark_price());
1483                    pos.update_premium_pnl(
1484                        instant,
1485                        pos.premium_pnl().sub(cc.from_signed(e.fundingCNS)),
1486                    );
1487                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1488                    chain!(
1489                        Some(StateEvents::position(
1490                            pos,
1491                            ctx,
1492                            PositionEventType::Deleveraged {
1493                                force_close: e.forceClose,
1494                                r#type: pos.r#type(),
1495                                entry_price: pos.entry_price(),
1496                                exit_price: perp
1497                                    .price_converter()
1498                                    .from_unsigned(e.deleveragePricePNS),
1499                                prev_size,
1500                                new_size: pos.size(),
1501                                deposit: pos.deposit(),
1502                                delta_pnl: pos.delta_pnl(),
1503                                premium_pnl: pos.premium_pnl(),
1504                            }
1505                        )),
1506                        if pos.r#type() == PositionType::Long {
1507                            perp.update_open_interest(instant, prev_size, pos.size());
1508                            Some(StateEvents::perpetual(
1509                                perp,
1510                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1511                            ))
1512                        } else {
1513                            None
1514                        },
1515                    )
1516                    .collect()
1517                } else {
1518                    vec![]
1519                },
1520                self.account(e.accountId).map(|acc| {
1521                    if e.endLotLNS == U256::ZERO {
1522                        acc.positions_mut()
1523                            .remove(&e.perpId.to::<types::PerpetualId>());
1524                    }
1525                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
1526                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
1527                }),
1528            )
1529            .collect(),
1530            ExchangeEvents::PositionDoesNotExist(_) => vec![],
1531            ExchangeEvents::PositionIncreased(e) => {
1532                if let Some((pos, perp)) = self.position(e.accountId, e.perpId)? {
1533                    let prev_size = pos.size();
1534                    pos.update_entry_price(instant, e.pricePNS, 0, perp.price_converter());
1535                    pos.update_size(instant, perp.size_converter().from_unsigned(e.endLotLNS));
1536                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1537                    pos.apply_mark_price(instant, perp.mark_price());
1538                    pos.update_premium_pnl(instant, D256::ZERO);
1539                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1540
1541                    chain!(
1542                        Some(StateEvents::position(
1543                            pos,
1544                            ctx,
1545                            PositionEventType::Increased {
1546                                entry_price: pos.entry_price(),
1547                                prev_size,
1548                                new_size: pos.size(),
1549                                deposit: pos.deposit(),
1550                            }
1551                        )),
1552                        if pos.r#type() == PositionType::Long {
1553                            perp.update_open_interest(instant, prev_size, pos.size());
1554                            Some(StateEvents::perpetual(
1555                                perp,
1556                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1557                            ))
1558                        } else {
1559                            None
1560                        },
1561                    )
1562                    .collect()
1563                } else {
1564                    vec![]
1565                }
1566            },
1567            ExchangeEvents::PositionIncreasedV2(e) => {
1568                if let Some((pos, perp)) = self.position(e.accountId, e.perpId)? {
1569                    let prev_size = pos.size();
1570                    pos.update_entry_price(
1571                        instant,
1572                        e.pricePNS,
1573                        e.priceResiduePNSQ16.to(),
1574                        perp.price_converter(),
1575                    );
1576                    pos.update_size(instant, perp.size_converter().from_unsigned(e.endLotLNS));
1577                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1578                    pos.apply_mark_price(instant, perp.mark_price());
1579                    pos.update_premium_pnl(instant, D256::ZERO);
1580                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1581
1582                    chain!(
1583                        Some(StateEvents::position(
1584                            pos,
1585                            ctx,
1586                            PositionEventType::Increased {
1587                                entry_price: pos.entry_price(),
1588                                prev_size,
1589                                new_size: pos.size(),
1590                                deposit: pos.deposit(),
1591                            }
1592                        )),
1593                        if pos.r#type() == PositionType::Long {
1594                            perp.update_open_interest(instant, prev_size, pos.size());
1595                            Some(StateEvents::perpetual(
1596                                perp,
1597                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1598                            ))
1599                        } else {
1600                            None
1601                        },
1602                    )
1603                    .collect()
1604                } else {
1605                    vec![]
1606                }
1607            },
1608            ExchangeEvents::PositionInverted(e) => {
1609                if let Some((pos, perp)) = self.position(e.accountId, e.perpId)? {
1610                    let prev_type = pos.r#type();
1611                    let prev_entry_price = pos.entry_price();
1612                    let prev_size = pos.size();
1613                    pos.update_type(instant, PositionType::from(e.positionType));
1614                    pos.update_entry_price(instant, e.pricePNS, 0, perp.price_converter());
1615                    pos.update_size(instant, perp.size_converter().from_unsigned(e.endLotLNS));
1616                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1617                    pos.apply_mark_price(instant, perp.mark_price());
1618                    pos.update_premium_pnl(instant, D256::ZERO);
1619                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1620                    if pos.r#type() == PositionType::Long {
1621                        perp.update_open_interest(instant, UD64::ZERO, pos.size());
1622                    } else {
1623                        perp.update_open_interest(instant, prev_size, UD64::ZERO);
1624                    }
1625                    vec![
1626                        StateEvents::position(
1627                            pos,
1628                            ctx,
1629                            PositionEventType::Closed {
1630                                r#type: prev_type,
1631                                entry_price: prev_entry_price,
1632                                exit_price: pos.entry_price(),
1633                                size: prev_size,
1634                                delta_pnl: cc.from_signed(e.deltaPnlCNS),
1635                                premium_pnl: cc.from_signed(e.fundingCNS),
1636                            },
1637                        ),
1638                        StateEvents::position(
1639                            pos,
1640                            ctx,
1641                            PositionEventType::Inverted {
1642                                r#type: pos.r#type(),
1643                                entry_price: pos.entry_price(),
1644                                prev_size,
1645                                new_size: pos.size(),
1646                                deposit: pos.deposit(),
1647                                delta_pnl: pos.delta_pnl(),
1648                                premium_pnl: pos.premium_pnl(),
1649                            },
1650                        ),
1651                        StateEvents::perpetual(
1652                            perp,
1653                            PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1654                        ),
1655                    ]
1656                } else {
1657                    vec![]
1658                }
1659            },
1660            ExchangeEvents::PositionLiquidated(e) => chain!(
1661                if let Some((pos, perp)) = self.position(e.posAccountId, e.perpId)? {
1662                    let prev_size = pos.size();
1663                    pos.update_size(instant, perp.size_converter().from_unsigned(e.posLotLNS));
1664                    pos.update_deposit(instant, cc.from_unsigned(e.posDepositCNS));
1665                    pos.apply_mark_price(instant, perp.mark_price());
1666                    pos.update_premium_pnl(
1667                        instant,
1668                        pos.premium_pnl().sub(cc.from_signed(e.fundingCNS)),
1669                    );
1670                    pos.apply_maintenance_margin(instant, perp.maintenance_margin());
1671                    chain!(
1672                        Some(StateEvents::position(
1673                            pos,
1674                            ctx,
1675                            PositionEventType::Liquidated {
1676                                r#type: pos.r#type(),
1677                                entry_price: pos.entry_price(),
1678                                exit_price: perp.price_converter().from_unsigned(e.liqPricePNS),
1679                                prev_size,
1680                                liquidated_size: perp.size_converter().from_unsigned(e.liqLotLNS),
1681                                new_size: pos.size(),
1682                                deposit: pos.deposit(),
1683                                delta_pnl: pos.delta_pnl(),
1684                                premium_pnl: pos.premium_pnl(),
1685                            }
1686                        )),
1687                        if pos.r#type() == PositionType::Long {
1688                            perp.update_open_interest(instant, prev_size, pos.size());
1689                            Some(StateEvents::perpetual(
1690                                perp,
1691                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1692                            ))
1693                        } else {
1694                            None
1695                        },
1696                    )
1697                    .collect()
1698                } else {
1699                    vec![]
1700                },
1701                self.account(e.posAccountId).map(|acc| {
1702                    if e.posLotLNS == U256::ZERO {
1703                        acc.positions_mut()
1704                            .remove(&e.perpId.to::<types::PerpetualId>());
1705                    }
1706                    acc.update_balance(instant, cc.from_unsigned(e.accBalanceCNS));
1707                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
1708                }),
1709            )
1710            .collect(),
1711            ExchangeEvents::PositionLiquidationCredit(e) => self
1712                .position(e.accountId, e.perpId)?
1713                .map(|(pos, _)| {
1714                    pos.update_deposit(instant, cc.from_unsigned(e.endDepositCNS));
1715                    StateEvents::position(
1716                        pos,
1717                        ctx,
1718                        PositionEventType::DepositUpdated(pos.deposit()),
1719                    )
1720                })
1721                .into_iter()
1722                .collect(),
1723            ExchangeEvents::PositionOpened(e) => {
1724                if let Some((acc, perp)) = self.account_perpetual(e.accountId, e.perpId) {
1725                    let pos = Position::opened(
1726                        instant,
1727                        perp.id(),
1728                        acc.id(),
1729                        PositionType::from(e.positionType),
1730                        e.pricePNS,
1731                        0,
1732                        perp.price_converter(),
1733                        perp.size_converter().from_unsigned(e.lotLNS),
1734                        cc.from_unsigned(e.depositCNS),
1735                        perp.maintenance_margin(),
1736                    );
1737                    let events = chain!(
1738                        Some(StateEvents::position(
1739                            &pos,
1740                            ctx,
1741                            PositionEventType::Opened {
1742                                r#type: pos.r#type(),
1743                                entry_price: pos.entry_price(),
1744                                size: pos.size(),
1745                                deposit: pos.deposit(),
1746                            }
1747                        )),
1748                        if pos.r#type() == PositionType::Long {
1749                            perp.update_open_interest(instant, UD64::ZERO, pos.size());
1750                            Some(StateEvents::perpetual(
1751                                perp,
1752                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1753                            ))
1754                        } else {
1755                            None
1756                        },
1757                    )
1758                    .collect();
1759                    acc.positions_mut().insert(perp.id(), pos);
1760                    events
1761                } else {
1762                    vec![]
1763                }
1764            },
1765            ExchangeEvents::PositionOpenedV2(e) => {
1766                if let Some((acc, perp)) = self.account_perpetual(e.accountId, e.perpId) {
1767                    let pos = Position::opened(
1768                        instant,
1769                        perp.id(),
1770                        acc.id(),
1771                        PositionType::from(e.positionType),
1772                        e.pricePNS,
1773                        e.priceResiduePNSQ16.to(),
1774                        perp.price_converter(),
1775                        perp.size_converter().from_unsigned(e.lotLNS),
1776                        cc.from_unsigned(e.depositCNS),
1777                        perp.maintenance_margin(),
1778                    );
1779                    let events = chain!(
1780                        Some(StateEvents::position(
1781                            &pos,
1782                            ctx,
1783                            PositionEventType::Opened {
1784                                r#type: pos.r#type(),
1785                                entry_price: pos.entry_price(),
1786                                size: pos.size(),
1787                                deposit: pos.deposit(),
1788                            }
1789                        )),
1790                        if pos.r#type() == PositionType::Long {
1791                            perp.update_open_interest(instant, UD64::ZERO, pos.size());
1792                            Some(StateEvents::perpetual(
1793                                perp,
1794                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1795                            ))
1796                        } else {
1797                            None
1798                        },
1799                    )
1800                    .collect();
1801                    acc.positions_mut().insert(perp.id(), pos);
1802                    events
1803                } else {
1804                    vec![]
1805                }
1806            },
1807            ExchangeEvents::PositionUnwound(e) => {
1808                if let Some((acc, perp)) = self.account_perpetual(e.accountId, e.perpId) {
1809                    let pos = acc
1810                        .positions_mut()
1811                        .remove(&perp.id())
1812                        .ok_or(DexError::PositionNotFound(acc.id(), perp.id()))?;
1813                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
1814                    chain!(
1815                        Some(StateEvents::position(
1816                            &pos,
1817                            ctx,
1818                            PositionEventType::Unwound {
1819                                r#type: pos.r#type(),
1820                                entry_price: pos.entry_price(),
1821                                exit_price: perp.price_converter().from_unsigned(e.pricePNS),
1822                                size: pos.size(),
1823                                fair_market_value: cc.from_signed(e.positionFmvCNS),
1824                                payment: cc.from_unsigned(e.paymentCNS),
1825                            }
1826                        )),
1827                        Some(StateEvents::account(
1828                            acc,
1829                            ctx,
1830                            AccountEventType::BalanceUpdated(acc.balance())
1831                        )),
1832                        if pos.r#type() == PositionType::Long {
1833                            perp.update_open_interest(instant, pos.size(), UD64::ZERO);
1834                            Some(StateEvents::perpetual(
1835                                perp,
1836                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1837                            ))
1838                        } else {
1839                            None
1840                        },
1841                    )
1842                    .collect()
1843                } else {
1844                    vec![]
1845                }
1846            },
1847            ExchangeEvents::PositionUnwoundV2(e) => {
1848                // Position is being removed; residue field is informational only.
1849                if let Some((acc, perp)) = self.account_perpetual(e.accountId, e.perpId) {
1850                    let pos = acc
1851                        .positions_mut()
1852                        .remove(&perp.id())
1853                        .ok_or(DexError::PositionNotFound(acc.id(), perp.id()))?;
1854                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
1855                    chain!(
1856                        Some(StateEvents::position(
1857                            &pos,
1858                            ctx,
1859                            PositionEventType::Unwound {
1860                                r#type: pos.r#type(),
1861                                entry_price: pos.entry_price(),
1862                                exit_price: perp.price_converter().from_unsigned(e.pricePNS),
1863                                size: pos.size(),
1864                                fair_market_value: cc.from_signed(e.positionFmvCNS),
1865                                payment: cc.from_unsigned(e.paymentCNS),
1866                            }
1867                        )),
1868                        Some(StateEvents::account(
1869                            acc,
1870                            ctx,
1871                            AccountEventType::BalanceUpdated(acc.balance())
1872                        )),
1873                        if pos.r#type() == PositionType::Long {
1874                            perp.update_open_interest(instant, pos.size(), UD64::ZERO);
1875                            Some(StateEvents::perpetual(
1876                                perp,
1877                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1878                            ))
1879                        } else {
1880                            None
1881                        },
1882                    )
1883                    .collect()
1884                } else {
1885                    vec![]
1886                }
1887            },
1888            ExchangeEvents::PositionUnwoundWithoutPayment(e) => {
1889                if let Some((acc, perp)) = self.account_perpetual(e.accountId, e.perpId) {
1890                    let pos = acc
1891                        .positions_mut()
1892                        .remove(&perp.id())
1893                        .ok_or(DexError::PositionNotFound(acc.id(), perp.id()))?;
1894                    chain!(
1895                        Some(StateEvents::position(
1896                            &pos,
1897                            ctx,
1898                            PositionEventType::Unwound {
1899                                r#type: pos.r#type(),
1900                                entry_price: pos.entry_price(),
1901                                exit_price: perp.price_converter().from_unsigned(e.pricePNS),
1902                                size: pos.size(),
1903                                fair_market_value: cc.from_signed(e.positionFmvCNS),
1904                                payment: UD128::ZERO,
1905                            }
1906                        )),
1907                        if pos.r#type() == PositionType::Long {
1908                            perp.update_open_interest(instant, pos.size(), UD64::ZERO);
1909                            Some(StateEvents::perpetual(
1910                                perp,
1911                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1912                            ))
1913                        } else {
1914                            None
1915                        },
1916                    )
1917                    .collect()
1918                } else {
1919                    vec![]
1920                }
1921            },
1922            ExchangeEvents::PositionUnwoundWithoutPaymentV2(e) => {
1923                // Position is being removed; residue field is informational only.
1924                if let Some((acc, perp)) = self.account_perpetual(e.accountId, e.perpId) {
1925                    let pos = acc
1926                        .positions_mut()
1927                        .remove(&perp.id())
1928                        .ok_or(DexError::PositionNotFound(acc.id(), perp.id()))?;
1929                    chain!(
1930                        Some(StateEvents::position(
1931                            &pos,
1932                            ctx,
1933                            PositionEventType::Unwound {
1934                                r#type: pos.r#type(),
1935                                entry_price: pos.entry_price(),
1936                                exit_price: perp.price_converter().from_unsigned(e.pricePNS),
1937                                size: pos.size(),
1938                                fair_market_value: cc.from_signed(e.positionFmvCNS),
1939                                payment: UD128::ZERO,
1940                            }
1941                        )),
1942                        if pos.r#type() == PositionType::Long {
1943                            perp.update_open_interest(instant, pos.size(), UD64::ZERO);
1944                            Some(StateEvents::perpetual(
1945                                perp,
1946                                PerpetualEventType::OpenInterestUpdated(perp.open_interest()),
1947                            ))
1948                        } else {
1949                            None
1950                        },
1951                    )
1952                    .collect()
1953                } else {
1954                    vec![]
1955                }
1956            },
1957            ExchangeEvents::PostOrderUnderMinimum(_) => self
1958                .err_ctx(ctx, event)?
1959                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::PostOrderUnderMinimum))
1960                .into_iter()
1961                .collect(),
1962            ExchangeEvents::PriceAdministratorUpdated(_) => vec![],
1963            ExchangeEvents::PriceMaxAgeUpdated(e) => {
1964                if let Some(perp) = self.perpetual(e.perpId) {
1965                    perp.update_price_max_age_sec(instant, e.maxAgeSec.to());
1966                }
1967                vec![]
1968            },
1969            ExchangeEvents::PriceOutOfRange(_) => self
1970                .err_ctx(ctx, event)
1971                .ok() // Used both for orders and mark/oracle prices
1972                .flatten()
1973                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::PriceOutOfRange))
1974                .into_iter()
1975                .collect(),
1976            ExchangeEvents::PriceSetDuringTriggerExec(_) => vec![],
1977            ExchangeEvents::PriceTolUpdated(_) => vec![],
1978            ExchangeEvents::ProtocolBalanceDeposit(_) => vec![],
1979            ExchangeEvents::ProtocolBalanceWithdraw(_) => vec![],
1980            ExchangeEvents::RecycleBalanceInsufficientSevere(_) => vec![],
1981            ExchangeEvents::RecycleFeeToAccount(_) => vec![],
1982            ExchangeEvents::RecycleFeeToProtocol(_) => vec![],
1983            ExchangeEvents::RecycleFeeUpdated(e) => {
1984                self.recycle_fee = cc.from_unsigned(e.recycleFeeCNS);
1985                vec![StateEvents::Exchange(ExchangeEvent::RecycleFeeUpdated(self.recycle_fee()))]
1986            },
1987            ExchangeEvents::ReportAgeExceedsLastUpdate(_) => vec![],
1988            ExchangeEvents::ReportExpiresTooSoon(_) => vec![],
1989            ExchangeEvents::ReportFromFuture(_) => vec![],
1990            ExchangeEvents::ReportPriceIsNegative(_) => vec![],
1991            ExchangeEvents::ResidueBalanceInsufficient(_) => vec![],
1992            ExchangeEvents::ResidueTransferred(_) => vec![],
1993            // Deprecated in v1.1.7.4, replayed from earlier history only
1994            ExchangeEvents::TakerFeeUpdated(e) => self
1995                .perpetual(e.perpId)
1996                .map(|perp| {
1997                    perp.update_base_taker_fee(
1998                        instant,
1999                        perp.fee_converter().from_unsigned(e.takerFeePer100K),
2000                    );
2001                    StateEvents::perpetual(
2002                        perp,
2003                        PerpetualEventType::TakerFeeUpdated(perp.taker_fee()),
2004                    )
2005                })
2006                .into_iter()
2007                .collect(),
2008            // Superseded by `TakerOrderFilledV2` in v1.1.7.4, replayed from
2009            // earlier history only, hence no builder fee
2010            ExchangeEvents::TakerOrderFilled(e) => self.apply_taker_order_filled(
2011                instant,
2012                event,
2013                ctx,
2014                RawTakerFill {
2015                    collat_price_pns: e.collatPricePNS,
2016                    lot_lns: e.lotLNS,
2017                    fee_cns: e.feeCNS,
2018                    builder_fee_cns: U256::ZERO,
2019                    balance_cns: e.balanceCNS,
2020                },
2021            )?,
2022            ExchangeEvents::TakerOrderFilledV2(e) => self.apply_taker_order_filled(
2023                instant,
2024                event,
2025                ctx,
2026                RawTakerFill {
2027                    collat_price_pns: e.collatPricePNS,
2028                    lot_lns: e.lotLNS,
2029                    fee_cns: e.feeCNS,
2030                    builder_fee_cns: e.builderFeeCNS,
2031                    balance_cns: e.balanceCNS,
2032                },
2033            )?,
2034            ExchangeEvents::ToleranceAdministratorUpdated(_) => vec![],
2035            ExchangeEvents::TransferAccountToProtocol(e) => self
2036                .account(e.accountId)
2037                .map(|acc| {
2038                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
2039                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
2040                })
2041                .into_iter()
2042                .collect(),
2043            ExchangeEvents::TransferPerpInsToProtocol(_) => vec![],
2044            ExchangeEvents::TransferPerpPosToProtocol(_) => vec![],
2045            ExchangeEvents::TransferProtocolToAccount(e) => self
2046                .account(e.accountId)
2047                .map(|acc| {
2048                    acc.update_balance(instant, cc.from_unsigned(e.balanceCNS));
2049                    StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
2050                })
2051                .into_iter()
2052                .collect(),
2053            ExchangeEvents::TransferProtocolToPerp(_) => vec![],
2054            ExchangeEvents::TransferProtocolToRecycleBal(_) => vec![],
2055            ExchangeEvents::TriggerDescIdTooLow(_) => vec![],
2056            ExchangeEvents::TriggerOrderExecution(_) => vec![],
2057            ExchangeEvents::TriggerOrderRequest(_) => vec![],
2058            ExchangeEvents::UnableToCancelOrder(_) => vec![],
2059            ExchangeEvents::UnityDescentThreshUpdated(_) => vec![],
2060            ExchangeEvents::UnspecifiedCollateral(_) => vec![],
2061            ExchangeEvents::UnwindCompleted(_) => vec![],
2062            ExchangeEvents::UnwindContractTrigger(_) => vec![],
2063            ExchangeEvents::UnwindInitializationCleared(_) => vec![],
2064            ExchangeEvents::UnwindInitialized(_) => vec![],
2065            ExchangeEvents::UnwindInsufficientBalance(_) => vec![],
2066            ExchangeEvents::UnwindIterationCompleted(_) => vec![],
2067            ExchangeEvents::UnwindPrepared(_) => vec![],
2068            ExchangeEvents::UnwindPreparationCleared(_) => vec![],
2069            ExchangeEvents::UnwindProcessInProgress(_) => vec![],
2070            ExchangeEvents::UpdateOracleFailed(_) => vec![],
2071            ExchangeEvents::Upgraded(_) => vec![],
2072            ExchangeEvents::ValueExceedsMaximum(_) => self
2073                .err_ctx(ctx, event)?
2074                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::ValueExceedsMaximum))
2075                .into_iter()
2076                .collect(),
2077            ExchangeEvents::WhitelistAddress(_) => vec![],
2078            ExchangeEvents::WhitelistingEnabledChanged(_) => vec![],
2079            ExchangeEvents::WithdrawRateLimitBypassSet(_) => vec![],
2080            ExchangeEvents::WithdrawRateLimitForceReset(_) => vec![],
2081            ExchangeEvents::WRLSMinWithdrawLimitUpdated(_) => vec![],
2082            ExchangeEvents::WRLSThousandthsTvlUpdated(_) => vec![],
2083            ExchangeEvents::WithdrawRateLimitReset(_) => vec![],
2084            ExchangeEvents::WrongAccountForOrder(_) => self
2085                .err_ctx(ctx, event)?
2086                .map(|ctx| StateEvents::order_error(ctx, OrderErrorType::WrongAccountForOrder))
2087                .into_iter()
2088                .collect(),
2089        })
2090    }
2091
2092    /// Starts tracking a newly listed perpetual contract, shared by the V1 and
2093    /// V2 listing events.
2094    fn add_perpetual(
2095        &mut self,
2096        instant: types::StateInstant,
2097        name: &str,
2098        symbol: &str,
2099        listing: RawContractAdded,
2100        fee_schedule: FeeSchedule,
2101    ) -> StateEvents {
2102        let perp = Perpetual::added(
2103            instant,
2104            listing.perp_id.to(),
2105            name.to_string(),
2106            symbol.to_string(),
2107            listing.status == 0, // PerpStatusEnum::Paused
2108            listing.price_decimals.to(),
2109            listing.lot_decimals.to(),
2110            listing.base_price_pns,
2111            fee_schedule,
2112            listing.init_margin_frac_hdths,
2113            listing.maint_margin_frac_hdths,
2114        );
2115        let event = StateEvents::perpetual(&perp, PerpetualEventType::Added);
2116        self.perpetuals.insert(perp.id(), perp);
2117        event
2118    }
2119
2120    /// Applies a maker fill, shared by the V1 and V2 event variants which
2121    /// differ only in carrying the builder fee earned on the fill.
2122    fn apply_maker_order_filled(
2123        &mut self,
2124        instant: types::StateInstant,
2125        event: &stream::RawEvent,
2126        ctx: &mut Option<OrderContext>,
2127        fill: RawMakerFill,
2128    ) -> Result<Vec<StateEvents>, DexError> {
2129        let cc = self.collateral_converter;
2130        Ok(chain!(
2131            if let Some((perp, order)) = self.order(fill.perp_id, fill.order_id)? {
2132                let fill_price = perp.price_converter().from_unsigned(fill.price_pns);
2133                let fill_size = perp.size_converter().from_unsigned(fill.lot_lns);
2134                let fee = cc.from_unsigned(fill.fee_cns);
2135                let builder_fee = cc.from_unsigned(fill.builder_fee_cns);
2136                perp.update_last_price(instant, fill_price);
2137                let clearing_remaining_order = if let Some(ctx) = ctx {
2138                    ctx.maker_fills.push(types::MakerFill {
2139                        log_index: event.log_index(),
2140                        maker_account_id: order.account_id(),
2141                        maker_order_id: order.order_id(),
2142                        maker_client_order_id: order.client_order_id(),
2143                        price: fill_price,
2144                        size: fill_size,
2145                        fee,
2146                        builder: order.builder(),
2147                        builder_fee,
2148                    });
2149                    let position_closed_by_smart_contract = if event.log_index() > 0 {
2150                        // Smart contract explicitly removes Close* order if position was
2151                        // closed, between `PositionClosed` and `MakerOrderFilled` events
2152                        // there can be a `RecycleFeeToAccount` event as well
2153                        match order.r#type() {
2154                            OrderType::CloseLong | OrderType::CloseShort => {
2155                                Some(event.log_index() - 1) == ctx.position_closed_at_log_index
2156                                    || Some(event.log_index() - 2)
2157                                        == ctx.position_closed_at_log_index
2158                            },
2159                            _ => false,
2160                        }
2161                    } else {
2162                        false
2163                    };
2164                    ctx.clearing_remaining_order | position_closed_by_smart_contract
2165                } else {
2166                    false
2167                };
2168                vec![
2169                    if order.size() > fill_size && !clearing_remaining_order {
2170                        let new_size = order.size() - fill_size;
2171                        perp.update_order(order.updated(
2172                            instant,
2173                            ctx,
2174                            None,
2175                            Some(new_size),
2176                            None,
2177                            None,
2178                        ))
2179                        .expect("order exists");
2180                        StateEvents::order(
2181                            perp,
2182                            &order,
2183                            ctx,
2184                            OrderEventType::Updated {
2185                                price: None,
2186                                size: Some(new_size),
2187                                expiry_block: None,
2188                            },
2189                        )
2190                    } else {
2191                        perp.remove_order(order.order_id()).expect("order exists");
2192                        StateEvents::order(perp, &order, ctx, OrderEventType::Removed)
2193                    },
2194                    StateEvents::order(
2195                        perp,
2196                        &order,
2197                        ctx,
2198                        OrderEventType::Filled {
2199                            fill_price,
2200                            fill_size,
2201                            fee,
2202                            builder_fee,
2203                            is_maker: true,
2204                        },
2205                    ),
2206                    StateEvents::perpetual(
2207                        perp,
2208                        PerpetualEventType::LastPriceUpdated(perp.last_price()),
2209                    ),
2210                ]
2211            } else {
2212                vec![]
2213            },
2214            self.account(fill.account_id).map(|acc| {
2215                acc.update_locked_balance(instant, cc.from_unsigned(fill.locked_balance_cns));
2216                StateEvents::account(
2217                    acc,
2218                    ctx,
2219                    AccountEventType::LockedBalanceUpdated(acc.locked_balance()),
2220                )
2221            }),
2222            self.account(fill.account_id).map(|acc| {
2223                acc.update_balance(instant, cc.from_unsigned(fill.balance_cns));
2224                StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
2225            }),
2226        )
2227        .collect())
2228    }
2229
2230    /// Applies a taker fill, shared by the V1 and V2 event variants which
2231    /// differ only in carrying the builder fee earned on the fill.
2232    fn apply_taker_order_filled(
2233        &mut self,
2234        instant: types::StateInstant,
2235        event: &stream::RawEvent,
2236        ctx: &mut Option<OrderContext>,
2237        fill: RawTakerFill,
2238    ) -> Result<Vec<StateEvents>, DexError> {
2239        let cc = self.collateral_converter;
2240        let c = ctx
2241            .as_ref()
2242            .ok_or(DexError::OrderContextExpected(event.tx_index(), event.log_index()))?;
2243        let taker_fee = cc.from_unsigned(fill.fee_cns);
2244        let taker_builder_fee = cc.from_unsigned(fill.builder_fee_cns);
2245        Ok(chain!(
2246            self.perpetuals
2247                .get(&c.perpetual_id)
2248                .map(|perp| StateEvents::Order(OrderEvent {
2249                    perpetual_id: perp.id(),
2250                    account_id: c.account_id,
2251                    request_id: Some(c.request_id),
2252                    client_order_id: Some(c.request_id),
2253                    order_id: None,
2254                    builder: c.builder,
2255                    r#type: OrderEventType::Filled {
2256                        fill_price: perp.price_converter().from_unsigned(fill.collat_price_pns),
2257                        fill_size: perp.size_converter().from_unsigned(fill.lot_lns),
2258                        fee: taker_fee,
2259                        builder_fee: taker_builder_fee,
2260                        is_maker: false,
2261                    },
2262                })),
2263            self.accounts.get_mut(&c.account_id).map(|acc| {
2264                acc.update_balance(instant, cc.from_unsigned(fill.balance_cns));
2265                StateEvents::account(acc, ctx, AccountEventType::BalanceUpdated(acc.balance()))
2266            }),
2267            iter::once(StateEvents::trade(c, taker_fee, taker_builder_fee)),
2268        )
2269        .collect())
2270    }
2271
2272    /// Registers a rewrite of a fee schedule, fanning it out to every tracked
2273    /// contract *currently pointing at it* - a contract keyed by the same id
2274    /// but resolving its fees elsewhere is left alone, as only
2275    /// `PerpFeeSchedIdSet` moves a contract between schedules.
2276    fn update_fee_schedule(
2277        &mut self,
2278        instant: types::StateInstant,
2279        schedule: FeeSchedule,
2280    ) -> Vec<StateEvents> {
2281        self.fee_schedules.set(schedule);
2282        chain!(
2283            iter::once(StateEvents::Exchange(ExchangeEvent::FeeScheduleUpdated(schedule))),
2284            self.perpetuals
2285                .values_mut()
2286                .filter(|perp| perp.fee_schedule().key() == schedule.key())
2287                .map(|perp| {
2288                    perp.update_fee_schedule(instant, schedule);
2289                    StateEvents::perpetual(
2290                        perp,
2291                        PerpetualEventType::FeeScheduleUpdated(perp.fee_schedule()),
2292                    )
2293                }),
2294        )
2295        .collect()
2296    }
2297
2298    fn apply_state_event(
2299        &mut self,
2300        instant: types::StateInstant,
2301        event: &StateEvents,
2302    ) -> Result<Vec<StateEvents>, DexError> {
2303        Ok(match event {
2304            StateEvents::Perpetual(pe) => {
2305                match pe.r#type {
2306                    PerpetualEventType::MaintenanceMarginFractionUpdated(maintenance_margin) => {
2307                        // Applying new maintenance margin to all tracked positions
2308                        self.accounts
2309                            .values_mut()
2310                            .filter_map(|acc| {
2311                                acc.positions_mut().get_mut(&pe.perpetual_id).map(|pos| {
2312                                    pos.apply_maintenance_margin(instant, maintenance_margin);
2313                                    StateEvents::position(
2314                                        pos,
2315                                        &None,
2316                                        PositionEventType::MaintenanceMarginUpdated(
2317                                            pos.maintenance_margin_requirement(),
2318                                        ),
2319                                    )
2320                                })
2321                            })
2322                            .collect()
2323                    },
2324                    _ => vec![],
2325                }
2326            },
2327            _ => vec![],
2328        })
2329    }
2330
2331    fn err_ctx<'c>(
2332        &self,
2333        ctx: &'c mut Option<OrderContext>,
2334        event: &stream::RawEvent,
2335    ) -> Result<Option<&'c OrderContext>, DexError> {
2336        let c = ctx
2337            .as_ref()
2338            .ok_or(DexError::OrderContextExpected(event.tx_index(), event.log_index()))?;
2339        Ok(self.accounts.contains_key(&c.account_id).then_some(c))
2340    }
2341
2342    fn ensure_account(&mut self, id: U256) {
2343        let id = id.to::<types::AccountId>();
2344        if self.track_all_accounts && !self.accounts.contains_key(&id) {
2345            self.accounts
2346                .insert(id, Account::untracked(types::StateInstant::default(), id));
2347        }
2348    }
2349
2350    fn account(&mut self, id: U256) -> Option<&mut Account> {
2351        self.ensure_account(id);
2352        self.accounts.get_mut(&id.to::<types::AccountId>())
2353    }
2354
2355    fn order(
2356        &mut self,
2357        perp_id: U256,
2358        ord_id: U256,
2359    ) -> Result<Option<(&mut Perpetual, Order)>, DexError> {
2360        let ord_id = std::num::NonZeroU16::new(ord_id.to::<u16>())
2361            .expect("ord_id in order lookup cannot be 0");
2362        Ok(if let Some(perp) = self.perpetuals.get_mut(&perp_id.to::<types::PerpetualId>()) {
2363            let ord = perp
2364                .get_order(ord_id)
2365                .copied()
2366                .ok_or(DexError::OrderNotFound(perp.id(), ord_id))?;
2367            Some((perp, ord))
2368        } else {
2369            None
2370        })
2371    }
2372
2373    fn perpetual(&mut self, id: U256) -> Option<&mut Perpetual> {
2374        self.perpetuals.get_mut(&id.to::<types::PerpetualId>())
2375    }
2376
2377    fn account_perpetual(
2378        &mut self,
2379        acc_id: U256,
2380        perp_id: U256,
2381    ) -> Option<(&mut Account, &mut Perpetual)> {
2382        self.ensure_account(acc_id);
2383        self.accounts
2384            .get_mut(&acc_id.to::<types::AccountId>())
2385            .zip(self.perpetuals.get_mut(&perp_id.to::<types::PerpetualId>()))
2386    }
2387
2388    fn position(
2389        &mut self,
2390        acc_id: U256,
2391        perp_id: U256,
2392    ) -> Result<Option<(&mut Position, &mut Perpetual)>, DexError> {
2393        self.ensure_account(acc_id);
2394        let acc_id = acc_id.to::<types::AccountId>();
2395        let perp_id = perp_id.to::<types::PerpetualId>();
2396        Ok(
2397            if let Some(acc) = self.accounts.get_mut(&acc_id)
2398                && let Some(perp) = self.perpetuals.get_mut(&perp_id)
2399            {
2400                let pos = acc
2401                    .positions_mut()
2402                    .get_mut(&perp_id)
2403                    .ok_or(DexError::PositionNotFound(acc_id, perp_id))?;
2404                Some((pos, perp))
2405            } else {
2406                None
2407            },
2408        )
2409    }
2410}
2411
2412#[cfg(feature = "display")]
2413impl std::fmt::Display for Exchange {
2414    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2415        use colored::Colorize;
2416        use tabled::{Table, settings::Style};
2417
2418        writeln!(
2419            f,
2420            "{}",
2421            format!(
2422                "{} | {}{} | {} (sdk {}) | chain {}",
2423                self.instant,
2424                if self.is_halted { "[HALTED] ".bold().bright_red() } else { Default::default() },
2425                self.chain.exchange(),
2426                self.features,
2427                Self::revision(),
2428                self.chain.chain_id(),
2429            )
2430            .bold()
2431            .purple(),
2432        )?;
2433
2434        let mut params = Table::from_iter(vec![vec![
2435            format!("Min Post: {}", self.min_post),
2436            format!("Min Settle: {}", self.min_settle),
2437            format!("Recycle Fee: {}", self.recycle_fee),
2438            format!("Funding Interval: {}", self.funding_interval_blocks),
2439        ]]);
2440        params.with(Style::modern());
2441        writeln!(f, "{params}")?;
2442
2443        // One row per registered schedule, the tier rates stacked taker over
2444        // maker. Pre-v1.1.7.4 contracts have no schedule registry - fees live on
2445        // the perpetual contract itself, and are rendered with it.
2446        if self.features.keyed_fee_schedules() {
2447            let mut fees = Table::from_iter(chain!(
2448                iter::once(
2449                    chain!(
2450                        iter::once("Fees (tkr/mkr)".to_string()),
2451                        (0..FEE_TIERS).map(|tier| format!("Tier {tier}")),
2452                    )
2453                    .collect::<Vec<_>>(),
2454                ),
2455                self.fee_schedules.schedules().map(|schedule| chain!(
2456                    iter::once(schedule.key().to_string()),
2457                    (0..FEE_TIERS as types::FeeTier).map(move |tier| format!(
2458                        "{}\n{}",
2459                        schedule.taker_fee(tier),
2460                        schedule.maker_fee(tier),
2461                    )),
2462                )
2463                .collect::<Vec<_>>()),
2464            ));
2465            fees.with(Style::modern());
2466            writeln!(f, "{fees}")?;
2467        }
2468        writeln!(f)?;
2469
2470        // Render perpetuals and accounts in alternate mode
2471        if f.alternate() && !self.perpetuals().is_empty() {
2472            let mut perpetuals: Vec<_> = self.perpetuals().values().collect();
2473            perpetuals.sort_by_key(|p| p.id());
2474            for perpetual in perpetuals {
2475                writeln!(f, "{:#}", perpetual)?;
2476            }
2477        }
2478
2479        if f.alternate() && !self.accounts().is_empty() {
2480            let mut accounts: Vec<_> = self.accounts().values().collect();
2481            accounts.sort_by_key(|a| a.id());
2482            for account in accounts {
2483                writeln!(f, "{:#}", account)?;
2484            }
2485        }
2486
2487        Ok(())
2488    }
2489}