1mod account;
19mod event;
20mod exchange;
21mod fee;
22mod l3_book;
23mod order;
24mod perpetual;
25mod position;
26mod version;
27
28use std::collections::{HashMap, hash_map};
29
30pub use account::*;
31use alloy::{
32 eips::BlockId,
33 primitives::U256,
34 providers::{CallItem, Provider},
35};
36pub use event::*;
37pub use exchange::*;
38use fastnum::UD64;
39pub use fee::*;
40use itertools::Itertools;
41pub use l3_book::*;
42pub use order::*;
43pub use perpetual::*;
44pub use position::*;
45pub use version::*;
46
47use crate::{
48 Chain,
49 abi::dex::{
50 self,
51 Exchange::{
52 Order as OrderV0, OrderV2, PerpetualInfo, PerpetualInfoV2, PositionInfo,
53 PositionInfoV2, getExchangeInfoReturn,
54 },
55 },
56 error::{DexError, ProviderError},
57 num, types,
58};
59
60const DEFAULT_ORDERS_PER_BATCH: usize = 1000;
64
65const DEFAULT_POSITIONS_PER_BATCH: usize = 1000;
69
70const PERPETUAL_PROBES_PER_BATCH: usize = 256;
74
75pub struct SnapshotBuilder<P> {
78 chain: Chain,
79 instance: dex::Exchange::ExchangeInstance<P>,
80 provider: P,
81 block_id: BlockId,
82 perpetuals: Vec<types::PerpetualId>,
83 accounts: Vec<types::AccountAddressOrID>,
84 all_positions: bool,
85 orders_per_batch: usize,
86 positions_per_batch: usize,
87}
88
89impl<P: Provider + Clone> SnapshotBuilder<P> {
90 pub fn new(chain: &Chain, provider: P) -> Self {
93 Self {
94 chain: chain.clone(),
95 instance: dex::Exchange::new(chain.exchange(), provider.clone()),
96 provider,
97 block_id: BlockId::Number(alloy::eips::BlockNumberOrTag::Safe),
98 perpetuals: chain.perpetuals.clone(),
99 accounts: vec![],
100 all_positions: false,
101 orders_per_batch: DEFAULT_ORDERS_PER_BATCH,
102 positions_per_batch: DEFAULT_POSITIONS_PER_BATCH,
103 }
104 }
105
106 pub fn at_block(mut self, block: BlockId) -> Self {
111 self.block_id = block;
112 self
113 }
114
115 pub fn with_perpetuals(mut self, perpetuals: Vec<types::PerpetualId>) -> Self {
120 self.perpetuals = perpetuals;
121 self
122 }
123
124 pub fn with_accounts(mut self, accounts: Vec<types::AccountAddressOrID>) -> Self {
127 self.accounts = accounts;
128 self.all_positions = false;
129 self
130 }
131
132 pub fn with_all_positions(mut self) -> Self {
136 self.accounts = vec![];
137 self.all_positions = true;
138 self
139 }
140
141 pub fn with_orders_per_batch(mut self, orders_per_batch: usize) -> Self {
145 self.orders_per_batch = orders_per_batch;
146 self
147 }
148
149 pub fn with_positions_per_batch(mut self, positions_per_batch: usize) -> Self {
152 self.positions_per_batch = positions_per_batch;
153 self
154 }
155
156 pub async fn build(mut self) -> Result<Exchange, DexError> {
158 let instant = self.normalize_block().await?;
160
161 let mut features = ContractFeatures::probe(
164 &self.instance,
165 self.block_id,
166 self.perpetuals.first().copied(),
167 )
168 .await;
169
170 if self.perpetuals.is_empty() {
173 self.perpetuals = discover_perpetuals(
174 &self.instance,
175 &self.provider,
176 self.block_id,
177 features,
178 self.chain.excluded_perpetuals(),
179 )
180 .await?;
181 if let Some(perp_id) = self.perpetuals.first().copied() {
184 features
185 .probe_v2_state_getters(&self.instance, self.block_id, perp_id)
186 .await;
187 }
188 }
189
190 let (
192 exchange_info,
193 funding_interval,
194 min_post,
195 min_settle,
196 recycle_fee,
197 is_halted,
198 num_of_accounts,
199 ) = self.exchange_info().await?;
200 let collateral_converter = num::Converter::new(exchange_info.collateralDecimals.to());
201
202 let (fee_schedules, perpetuals) =
207 futures::try_join!(self.fee_schedules(features), self.perpetuals(instant, features))?;
208
209 let accounts = if !self.accounts.is_empty() {
210 self.accounts(instant, &perpetuals, collateral_converter, features)
212 .await?
213 } else if self.all_positions {
214 self.position_accounts(
217 instant,
218 &perpetuals,
219 num_of_accounts.to(),
220 collateral_converter,
221 features,
222 )
223 .await?
224 } else {
225 HashMap::new()
226 };
227
228 Ok(Exchange::new(
229 self.chain.clone(),
230 instant,
231 features,
232 collateral_converter,
233 funding_interval.to(),
234 collateral_converter.from_unsigned(min_post),
235 collateral_converter.from_unsigned(min_settle),
236 collateral_converter.from_unsigned(recycle_fee),
237 fee_schedules,
238 perpetuals,
239 accounts,
240 is_halted,
241 self.all_positions,
242 ))
243 }
244
245 async fn fee_schedules(
258 &self,
259 features: ContractFeatures,
260 ) -> Result<FeeScheduleRegistry, DexError> {
261 if !features.keyed_fee_schedules() {
262 return Ok(FeeScheduleRegistry::new(
263 FeeSchedule::flat(FeeScheduleKey::Default, UD64::ZERO, UD64::ZERO),
264 FeeSchedule::flat(FeeScheduleKey::RwaDefault, UD64::ZERO, UD64::ZERO),
265 HashMap::new(),
266 ));
267 }
268 let fee_converter = features.fee_rate_converter();
272 let (default_call, rwa_call) = (
273 self.instance
274 .getDefaultPerpFeeSchedule()
275 .block(self.block_id),
276 self.instance
277 .getFeeScheduleById(FeeScheduleKey::RwaDefault.to_raw())
278 .block(self.block_id),
279 );
280 let custom_calls = self.perpetuals.iter().map(|perp_id| {
281 let key = FeeScheduleKey::Custom(*perp_id);
282 let call = self
283 .instance
284 .getFeeScheduleById(key.to_raw())
285 .block(self.block_id);
286 async move {
287 call.call().await.map(|schedule| {
288 (
289 *perp_id,
290 FeeSchedule::new(
291 key,
292 schedule.takerFeesPer100K,
293 schedule.makerFeesPer100K,
294 fee_converter,
295 ),
296 )
297 })
298 }
299 });
300 let (default, rwa, custom) = futures::try_join!(
301 default_call.call().into_future(),
302 rwa_call.call().into_future(),
303 futures::future::try_join_all(custom_calls),
304 )
305 .map_err(|err| DexError::Provider(err.into()))?;
306 Ok(FeeScheduleRegistry::new(
307 FeeSchedule::new(
308 FeeScheduleKey::Default,
309 default.takerFeesPer100K,
310 default.makerFeesPer100K,
311 fee_converter,
312 ),
313 FeeSchedule::new(
314 FeeScheduleKey::RwaDefault,
315 rwa.takerFeesPer100K,
316 rwa.makerFeesPer100K,
317 fee_converter,
318 ),
319 custom.into_iter().collect(),
320 ))
321 }
322
323 async fn fetch_fee_schedule(
329 &self,
330 perp_id: U256,
331 features: ContractFeatures,
332 ) -> Result<FeeSchedule, alloy::contract::Error> {
333 let fee_converter = features.fee_rate_converter();
334 if features.keyed_fee_schedules() {
335 self.instance
336 .getPerpFeeSchedule(perp_id)
337 .block(self.block_id)
338 .call()
339 .await
340 .map(|schedule| {
341 FeeSchedule::new(
342 FeeScheduleKey::from_raw(schedule.feeSchedId),
343 schedule.takerFeesPer100K,
344 schedule.makerFeesPer100K,
345 fee_converter,
346 )
347 })
348 } else {
349 let (maker_fee_call, taker_fee_call) = (
350 self.instance.getMakerFee(perp_id).block(self.block_id),
351 self.instance.getTakerFee(perp_id).block(self.block_id),
352 );
353 let (maker_fee, taker_fee) = futures::try_join!(
354 maker_fee_call.call().into_future(),
355 taker_fee_call.call().into_future(),
356 )?;
357 Ok(FeeSchedule::flat(
358 FeeScheduleKey::Default,
359 fee_converter.from_unsigned(taker_fee),
360 fee_converter.from_unsigned(maker_fee),
361 ))
362 }
363 }
364
365 async fn fetch_perpetual_info(
369 &self,
370 perp_id: U256,
371 features: ContractFeatures,
372 ) -> Result<PerpetualInfoV2, alloy::contract::Error> {
373 if features.v2_state_getters() {
374 self.instance
375 .getPerpetualInfoV2(perp_id)
376 .block(self.block_id)
377 .call()
378 .await
379 } else {
380 self.instance
381 .getPerpetualInfo(perp_id)
382 .block(self.block_id)
383 .call()
384 .await
385 .map(perpetual_info_v0_to_v2)
386 }
387 }
388
389 async fn fetch_position_info(
393 &self,
394 perp_id: U256,
395 account_id: U256,
396 features: ContractFeatures,
397 ) -> Result<PositionInfoV2, alloy::contract::Error> {
398 if features.v2_state_getters() {
399 self.instance
400 .getPositionV2(perp_id, account_id)
401 .block(self.block_id)
402 .call()
403 .await
404 .map(|r| r.positionInfo)
405 } else {
406 self.instance
407 .getPosition(perp_id, account_id)
408 .block(self.block_id)
409 .call()
410 .await
411 .map(|r| position_info_v0_to_v2(r.positionInfo))
412 }
413 }
414
415 async fn normalize_block(&mut self) -> Result<types::StateInstant, DexError> {
416 let block_header = self
419 .provider
420 .get_block(self.block_id)
421 .await
422 .map_err(|err| DexError::Provider(err.into()))?
423 .map(|b| b.into_header())
424 .ok_or(DexError::Provider(ProviderError::InvalidRequest(
425 "block not found".to_string(),
426 )))?;
427 self.block_id = BlockId::number(block_header.number);
428 Ok(types::StateInstant::new(block_header.number, block_header.timestamp))
429 }
430
431 async fn exchange_info(
432 &self,
433 ) -> Result<(getExchangeInfoReturn, U256, U256, U256, U256, bool, U256), DexError> {
434 let (
435 exchange_info_call,
436 funding_interval_call,
437 min_post_call,
438 min_settle_call,
439 recycle_fee_call,
440 is_halted_call,
441 num_of_accounts_call,
442 ) = (
443 self.instance.getExchangeInfo().block(self.block_id),
444 self.instance.getFundingInterval().block(self.block_id),
445 self.instance.getMinimumPostCNS().block(self.block_id),
446 self.instance.getMinimumSettleCNS().block(self.block_id),
447 self.instance.getRecycleFeeCNS().block(self.block_id),
448 self.instance.isHalted().block(self.block_id),
449 self.instance.numberOfAccounts().block(self.block_id),
453 );
454 futures::try_join!(
455 exchange_info_call.call().into_future(),
456 funding_interval_call.call().into_future(),
457 min_post_call.call().into_future(),
458 min_settle_call.call().into_future(),
459 recycle_fee_call.call().into_future(),
460 is_halted_call.call().into_future(),
461 num_of_accounts_call.call().into_future(),
462 )
463 .map_err(|err| DexError::Provider(err.into()))
464 }
465
466 async fn perpetuals(
467 &self,
468 instant: types::StateInstant,
469 features: ContractFeatures,
470 ) -> Result<HashMap<types::PerpetualId, perpetual::Perpetual>, DexError> {
471 let perpetual_futs = self.perpetuals.iter().map(|perp_id| async move {
472 let pid = U256::from(*perp_id);
473 let margins_call = self
474 .instance
475 .getMarginFractions(pid, U256::ZERO)
476 .block(self.block_id);
477
478 futures::try_join!(
479 self.fetch_perpetual_info(pid, features),
480 self.fetch_fee_schedule(pid, features),
481 margins_call.call().into_future(),
482 )
483 .map(|(perp_info, fee_schedule, margins)| (*perp_id, perp_info, fee_schedule, margins))
484 });
485
486 let mut perpetuals = futures::future::try_join_all(perpetual_futs)
487 .await
488 .map_err(|err| DexError::Provider(err.into()))?
489 .into_iter()
490 .map(|(perp_id, perp_info, fee_schedule, margins)| {
491 let perp = Perpetual::new(
492 instant,
493 perp_id,
494 &perp_info,
495 fee_schedule,
496 margins.perpInitMarginFracHdths,
497 margins.perpMaintMarginFracHdths,
498 );
499 (perp_id, perp)
500 })
501 .collect::<HashMap<_, _>>();
502
503 for perp in perpetuals.values_mut() {
505 self.perpetual_orders(perp, features).await?;
506 }
507
508 Ok(perpetuals)
509 }
510
511 async fn perpetual_orders(
512 &self,
513 perp: &mut perpetual::Perpetual,
514 features: ContractFeatures,
515 ) -> Result<(), DexError> {
516 let pid = U256::from(perp.id());
517 let order_id_index = self
518 .instance
519 .getOrderIdIndex(pid)
520 .block(self.block_id)
521 .call()
522 .await
523 .map_err(|err| DexError::Provider(err.into()))?;
524
525 let order_ids = order_id_index
526 .leaves
527 .into_iter()
528 .enumerate()
529 .flat_map(|(leaf, bitmap)| {
530 ((if leaf == 0 { 1 } else { 0 })..U256::BITS)
533 .filter(move |bit| bitmap.bit(*bit))
534 .map(move |bit| {
535 let id = (leaf * U256::BITS + bit) as u16;
536 std::num::NonZeroU16::new(id).expect("order id from bitmap cannot be 0")
538 })
539 })
540 .collect::<Vec<_>>();
541
542 let orders = self.fetch_orders(pid, &order_ids, features).await?;
543
544 let (instant, base_price, price_converter, size_converter, leverage_converter) = (
545 perp.instant(),
546 perp.base_price(),
547 perp.price_converter(),
548 perp.size_converter(),
549 perp.leverage_converter(),
550 );
551
552 let orders: Vec<Order> = orders
555 .into_iter()
556 .map(|ord| {
557 Order::from_snapshot(
558 instant,
559 ord,
560 base_price,
561 price_converter,
562 size_converter,
563 leverage_converter,
564 )
565 })
566 .collect::<Result<Vec<_>, _>>()
567 .map_err(|err| DexError::OrderParse(perp.id(), err))?;
568
569 perp.add_orders_from_snapshot(orders)
570 }
571
572 async fn fetch_orders(
577 &self,
578 perp_id: U256,
579 order_ids: &[types::OrderId],
580 features: ContractFeatures,
581 ) -> Result<Vec<OrderV2>, DexError> {
582 let order_ids = order_ids.to_vec();
583 if features.builder_attribution() {
584 aggregate_batched(order_ids, self.orders_per_batch, |chunk| {
585 let multicall = self
586 .provider
587 .multicall()
588 .block(self.block_id)
589 .dynamic()
590 .extend(
591 chunk
592 .iter()
593 .map(|oid| self.instance.getOrderV2(perp_id, U256::from(oid.get()))),
594 );
595 async move { multicall.aggregate().await }
596 })
597 .await
598 } else {
599 Ok(aggregate_batched(order_ids, self.orders_per_batch, |chunk| {
600 let multicall = self
601 .provider
602 .multicall()
603 .block(self.block_id)
604 .dynamic()
605 .extend(
606 chunk
607 .iter()
608 .map(|oid| self.instance.getOrder(perp_id, U256::from(oid.get()))),
609 );
610 async move { multicall.aggregate().await }
611 })
612 .await?
613 .into_iter()
614 .map(order_v0_to_v2)
615 .collect())
616 }
617 }
618
619 async fn accounts(
620 &self,
621 instant: types::StateInstant,
622 perpetuals: &HashMap<types::PerpetualId, perpetual::Perpetual>,
623 collateral_converter: num::Converter,
624 features: ContractFeatures,
625 ) -> Result<HashMap<types::AccountId, Account>, DexError> {
626 let account_futs = self.accounts.iter().map(|acc| async move {
627 let acc_info = match acc {
628 types::AccountAddressOrID::Address(addr) => self
629 .instance
630 .getAccountByAddr(*addr)
631 .block(self.block_id)
632 .call()
633 .await
634 .map_err(|err| DexError::Provider(err.into()))?,
635 types::AccountAddressOrID::ID(id) => self
636 .instance
637 .getAccountById(U256::from(*id))
638 .block(self.block_id)
639 .call()
640 .await
641 .map_err(|err| DexError::Provider(err.into()))?,
642 };
643 let fee_tier = self
644 .fetch_account_fee_tier(acc_info.accountId, features)
645 .await?;
646 let perps_with_positions = perpetuals_with_position(&acc_info.positions);
647 let position_futs = perps_with_positions.iter().map(|perp_id| async {
648 self.fetch_position_info(U256::from(*perp_id), acc_info.accountId, features)
649 .await
650 .map(|pos_info| (*perp_id, pos_info))
651 .map_err(|err| DexError::Provider(err.into()))
652 });
653 let positions = futures::future::try_join_all(position_futs).await?;
654 Ok::<_, DexError>((acc_info.accountId, acc_info, fee_tier, positions))
655 });
656
657 Ok(futures::future::try_join_all(account_futs)
658 .await?
659 .into_iter()
660 .map(|(acc_id, acc_info, fee_tier, positions)| {
661 (
662 acc_id.to(),
663 Account::new(
664 instant,
665 acc_id.to(),
666 &acc_info,
667 fee_tier,
668 positions
669 .into_iter()
670 .filter_map(|(perp_id, pos_info)| {
671 perpetuals.get(&perp_id).map(|perp| {
672 (
673 perp_id,
674 Position::new(
675 instant,
676 perp_id,
677 &pos_info,
678 collateral_converter,
679 perp.price_converter(),
680 perp.size_converter(),
681 perp.maintenance_margin(),
682 ),
683 )
684 })
685 })
686 .collect(),
687 collateral_converter,
688 ),
689 )
690 })
691 .collect())
692 }
693
694 async fn fetch_account_fee_tier(
697 &self,
698 account_id: U256,
699 features: ContractFeatures,
700 ) -> Result<Option<types::FeeTier>, DexError> {
701 if !features.keyed_fee_schedules() {
702 return Ok(None);
703 }
704 self.instance
705 .getAccountFeeTier(account_id)
706 .block(self.block_id)
707 .call()
708 .await
709 .map(|tier| Some(tier.to()))
710 .map_err(|err| DexError::Provider(err.into()))
711 }
712
713 async fn position_accounts(
714 &self,
715 instant: types::StateInstant,
716 perpetuals: &HashMap<types::PerpetualId, perpetual::Perpetual>,
717 num_accounts: usize,
718 collateral_converter: num::Converter,
719 features: ContractFeatures,
720 ) -> Result<HashMap<types::AccountId, Account>, DexError> {
721 let mut accounts: HashMap<types::AccountId, Account> = HashMap::new();
722 for (perp_id, perp) in perpetuals {
723 let pid = U256::from(*perp_id);
724 let infos = self
725 .fetch_position_infos_for_perp(pid, num_accounts, features)
726 .await?;
727 for info in infos {
728 if info.lotLNS.is_zero() {
729 continue;
730 }
731 let position = Position::new(
732 instant,
733 *perp_id,
734 &info,
735 collateral_converter,
736 perp.price_converter(),
737 perp.size_converter(),
738 perp.maintenance_margin(),
739 );
740 match accounts.entry(info.accountId.to()) {
741 hash_map::Entry::Occupied(mut e) => {
742 e.get_mut().positions_mut().insert(*perp_id, position);
743 },
744 hash_map::Entry::Vacant(e) => {
745 e.insert(Account::from_position(instant, position));
746 },
747 }
748 }
749 }
750
751 Ok(accounts)
752 }
753
754 async fn fetch_position_infos_for_perp(
757 &self,
758 perp_id: U256,
759 num_accounts: usize,
760 features: ContractFeatures,
761 ) -> Result<Vec<PositionInfoV2>, DexError> {
762 let account_ids = (1..num_accounts + 1).collect::<Vec<_>>();
763 if features.v2_state_getters() {
764 Ok(aggregate_batched(account_ids, self.positions_per_batch, |chunk| {
765 let multicall = self
766 .provider
767 .multicall()
768 .block(self.block_id)
769 .dynamic()
770 .extend(
771 chunk
772 .iter()
773 .map(|aid| self.instance.getPositionV2(perp_id, U256::from(*aid))),
774 );
775 async move { multicall.aggregate().await }
776 })
777 .await?
778 .into_iter()
779 .map(|r| r.positionInfo)
780 .collect())
781 } else {
782 Ok(aggregate_batched(account_ids, self.positions_per_batch, |chunk| {
783 let multicall = self
784 .provider
785 .multicall()
786 .block(self.block_id)
787 .dynamic()
788 .extend(
789 chunk
790 .iter()
791 .map(|aid| self.instance.getPosition(perp_id, U256::from(*aid))),
792 );
793 async move { multicall.aggregate().await }
794 })
795 .await?
796 .into_iter()
797 .map(|r| position_info_v0_to_v2(r.positionInfo))
798 .collect())
799 }
800 }
801}
802
803async fn aggregate_batched<T, R, F, Fut>(
820 items: Vec<T>,
821 batch_size: usize,
822 call: F,
823) -> Result<Vec<R>, DexError>
824where
825 T: Clone,
826 F: Fn(Vec<T>) -> Fut,
827 Fut: Future<Output = Result<Vec<R>, alloy::providers::MulticallError>>,
828{
829 let mut pending = items
832 .chunks(batch_size.max(1))
833 .enumerate()
834 .map(|(i, chunk)| (i * batch_size, chunk.to_vec()))
835 .collect::<Vec<_>>();
836 let mut fetched: Vec<(usize, Vec<R>)> = Vec::with_capacity(pending.len());
837
838 while !pending.is_empty() {
839 let results =
840 futures::future::join_all(pending.iter().map(|(_, chunk)| call(chunk.clone()))).await;
841 let mut retry = Vec::new();
842 for ((offset, chunk), result) in pending.into_iter().zip(results) {
843 match result {
844 Ok(values) => fetched.push((offset, values)),
845 Err(_) if chunk.len() > 1 => {
846 let mid = chunk.len() / 2;
847 retry.push((offset + mid, chunk[mid..].to_vec()));
848 retry.push((offset, chunk[..mid].to_vec()));
849 },
850 Err(err) => return Err(DexError::Provider(err.into())),
851 }
852 }
853 pending = retry;
854 }
855
856 fetched.sort_by_key(|(offset, _)| *offset);
857 Ok(fetched.into_iter().flat_map(|(_, values)| values).collect())
858}
859
860pub async fn listed_perpetuals<P: Provider + Clone>(
866 chain: &Chain,
867 provider: P,
868 block_id: BlockId,
869) -> Result<Vec<types::PerpetualId>, DexError> {
870 let instance = dex::Exchange::new(chain.exchange(), provider.clone());
871 let features = ContractFeatures::probe(&instance, block_id, None).await;
872 discover_perpetuals(&instance, &provider, block_id, features, chain.excluded_perpetuals()).await
873}
874
875async fn discover_perpetuals<P: Provider + Clone>(
884 instance: &dex::Exchange::ExchangeInstance<P>,
885 provider: &P,
886 block_id: BlockId,
887 features: ContractFeatures,
888 excluded: &[types::PerpetualId],
889) -> Result<Vec<types::PerpetualId>, DexError> {
890 if features.perpetual_discovery() {
891 let bitmap = instance
892 .getPerpetualExistsBitmap()
893 .block(block_id)
894 .call()
895 .await
896 .map_err(|err| DexError::Provider(err.into()))?;
897 return Ok(bitmap
898 .into_iter()
899 .enumerate()
900 .flat_map(|(word, bits)| {
901 (0..U256::BITS).filter_map(move |bit| {
902 let perp_id = (word * U256::BITS + bit) as types::PerpetualId;
903 (bits.bit(bit) && perp_id <= types::MAX_PERPETUAL_ID).then_some(perp_id)
904 })
905 })
906 .filter(|perp_id| !excluded.contains(perp_id))
907 .collect());
908 }
909
910 let probe_batch_futs = (0..=types::MAX_PERPETUAL_ID)
911 .filter(|perp_id| !excluded.contains(perp_id))
912 .chunks(PERPETUAL_PROBES_PER_BATCH)
913 .into_iter()
914 .map(|chunk| {
915 let perp_ids = chunk.collect::<Vec<_>>();
916 let multicall = provider
917 .multicall()
918 .block(block_id)
919 .dynamic::<dex::Exchange::getMarginFractionsCall>()
920 .extend_calls(perp_ids.iter().map(|perp_id| {
923 CallItem::from(instance.getMarginFractions(U256::from(*perp_id), U256::ZERO))
924 .with_failure_allowed()
925 }));
926 async move { multicall.aggregate3().await.map(|res| (perp_ids, res)) }
927 })
928 .collect::<Vec<_>>();
929
930 Ok(futures::future::try_join_all(probe_batch_futs)
931 .await
932 .map_err(|err| DexError::Provider(err.into()))?
933 .into_iter()
934 .flat_map(|(perp_ids, results)| {
935 perp_ids
936 .into_iter()
937 .zip(results)
938 .filter_map(|(perp_id, result)| result.is_ok().then_some(perp_id))
939 })
940 .collect())
941}
942
943fn position_info_v0_to_v2(v0: PositionInfo) -> PositionInfoV2 {
944 PositionInfoV2 {
945 accountId: v0.accountId,
946 nextNodeId: v0.nextNodeId,
947 prevNodeId: v0.prevNodeId,
948 positionType: v0.positionType,
949 depositCNS: v0.depositCNS,
950 pricePNS: v0.pricePNS,
951 lotLNS: v0.lotLNS,
952 entryBlock: v0.entryBlock,
953 pnlCNS: v0.pnlCNS,
954 deltaPnlCNS: v0.deltaPnlCNS,
955 premiumPnlCNS: v0.premiumPnlCNS,
956 priceResiduePNSQ16: U256::ZERO,
957 }
958}
959
960fn order_v0_to_v2(v0: OrderV0) -> OrderV2 {
961 OrderV2 {
962 accountId: v0.accountId,
963 orderType: v0.orderType,
964 priceONS: v0.priceONS,
965 lotLNS: v0.lotLNS,
966 recycleFeeRaw: v0.recycleFeeRaw,
967 expiryBlock: v0.expiryBlock,
968 leverageHdths: v0.leverageHdths,
969 orderId: v0.orderId,
970 prevOrderId: v0.prevOrderId,
971 nextOrderId: v0.nextOrderId,
972 maxNegPnlCollatBPS: v0.maxNegPnlCollatBPS,
973 builderId: 0,
974 builderFeePer100K: 0,
975 }
976}
977
978fn perpetual_info_v0_to_v2(v0: PerpetualInfo) -> PerpetualInfoV2 {
979 PerpetualInfoV2 {
980 name: v0.name,
981 symbol: v0.symbol,
982 priceDecimals: v0.priceDecimals,
983 lotDecimals: v0.lotDecimals,
984 linkFeedId: v0.linkFeedId,
985 priceTolPer100K: v0.priceTolPer100K,
986 marginTol: v0.marginTol,
987 marginTolDecimals: v0.marginTolDecimals,
988 refPriceMaxAgeSec: v0.refPriceMaxAgeSec,
989 positionBalanceCNS: v0.positionBalanceCNS,
990 insuranceBalanceCNS: v0.insuranceBalanceCNS,
991 markPNS: v0.markPNS,
992 markTimestamp: v0.markTimestamp,
993 lastPNS: v0.lastPNS,
994 lastTimestamp: v0.lastTimestamp,
995 oraclePNS: v0.oraclePNS,
996 oracleTimestampSec: v0.oracleTimestampSec,
997 longOpenInterestLNS: v0.longOpenInterestLNS,
998 shortOpenInterestLNS: v0.shortOpenInterestLNS,
999 fundingStartBlock: v0.fundingStartBlock,
1000 fundingRatePct100k: v0.fundingRatePct100k,
1001 absFundingClampPctPer100K: v0.absFundingClampPctPer100K,
1002 status: v0.status,
1003 basePricePNS: v0.basePricePNS,
1004 maxBidPriceONS: v0.maxBidPriceONS,
1005 minBidPriceONS: v0.minBidPriceONS,
1006 maxAskPriceONS: v0.maxAskPriceONS,
1007 minAskPriceONS: v0.minAskPriceONS,
1008 numOrders: v0.numOrders,
1009 ignOracle: v0.ignOracle,
1010 fundingSumScalingExp: U256::ZERO,
1011 }
1012}