1use crate::client::Http;
22use crate::client::evm::abi::{self, AbiError, Token};
23use crate::client::evm::rpc::{BlockTag, CallRequest, Rpc, RpcError};
24use crate::client::evm::u256::{B256, U256};
25use crate::utils::{Address, Dec};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct Spec {
30 pub at: u16,
32 pub name: &'static str,
34 pub args: &'static str,
35 pub returns: &'static str,
36}
37
38const POSITION: &str = "(int64 szi, uint64 entryNtl, int64 isolatedRawUsd, uint32 leverage, bool isIsolated)";
39
40pub const ALL: [Spec; 21] = [
42 Spec { at: 0x800, name: "position", args: "address user, uint16 perp", returns: POSITION },
43 Spec {
44 at: 0x801,
45 name: "spotBalance",
46 args: "address user, uint64 token",
47 returns: "(uint64 total, uint64 hold, uint64 entryNtl)",
48 },
49 Spec {
50 at: 0x802,
51 name: "userVaultEquity",
52 args: "address user, address vault",
53 returns: "(uint64 equity, uint64 lockedUntilTimestamp)",
54 },
55 Spec { at: 0x803, name: "withdrawable", args: "address user", returns: "(uint64 withdrawable)" },
56 Spec {
57 at: 0x804,
58 name: "delegations",
59 args: "address user",
60 returns: "(address validator, uint64 amount, uint64 lockedUntilTimestamp)[]",
61 },
62 Spec {
63 at: 0x805,
64 name: "delegatorSummary",
65 args: "address user",
66 returns: "(uint64 delegated, uint64 undelegated, uint64 totalPendingWithdrawal, uint64 nPendingWithdrawals)",
67 },
68 Spec { at: 0x806, name: "markPx", args: "uint32 index", returns: "uint64" },
69 Spec { at: 0x807, name: "oraclePx", args: "uint32 index", returns: "uint64" },
70 Spec { at: 0x808, name: "spotPx", args: "uint32 index", returns: "uint64" },
71 Spec { at: 0x809, name: "l1BlockNumber", args: "", returns: "uint64" },
72 Spec {
73 at: 0x80a,
74 name: "perpAssetInfo",
75 args: "uint32 perp",
76 returns: "(string coin, uint32 marginTableId, uint8 szDecimals, uint8 maxLeverage, bool onlyIsolated)",
77 },
78 Spec { at: 0x80b, name: "spotInfo", args: "uint32 spot", returns: "(string name, uint64[2] tokens)" },
79 Spec {
80 at: 0x80c,
81 name: "tokenInfo",
82 args: "uint32 token",
83 returns: "(string name, uint64[] spots, uint64 deployerTradingFeeShare, address deployer, address evmContract, uint8 szDecimals, uint8 weiDecimals, int8 evmExtraWeiDecimals)",
84 },
85 Spec {
86 at: 0x80d,
87 name: "tokenSupply",
88 args: "uint32 token",
89 returns: "(uint64 maxSupply, uint64 totalSupply, uint64 circulatingSupply, uint64 futureEmissions, (address user, uint64 balance)[] nonCirculatingUserBalances)",
90 },
91 Spec { at: 0x80e, name: "bbo", args: "uint32 asset", returns: "(uint64 bid, uint64 ask)" },
92 Spec {
93 at: 0x80f,
94 name: "accountMarginSummary",
95 args: "uint32 perp_dex_index, address user",
96 returns: "(int64 accountValue, uint64 marginUsed, uint64 ntlPos, int64 rawUsd)",
97 },
98 Spec { at: 0x810, name: "coreUserExists", args: "address user", returns: "(bool exists)" },
99 Spec {
100 at: 0x811,
101 name: "borrowLendUserState",
102 args: "address user, uint64 token",
103 returns: "((uint64 basis, uint64 value) borrow, (uint64 basis, uint64 value) supply)",
104 },
105 Spec {
106 at: 0x812,
107 name: "borrowLendReserveState",
108 args: "uint64 token",
109 returns: "(uint64 borrowYearlyRateBps, uint64 supplyYearlyRateBps, uint64 balance, uint64 utilizationBps, uint64 oraclePx, uint64 ltvBps, uint64 totalSupplied, uint64 totalBorrowed)",
110 },
111 Spec { at: 0x813, name: "position2", args: "address user, uint32 perp", returns: POSITION },
112 Spec {
113 at: 0x814,
114 name: "outcomeStatus",
115 args: "uint32 outcome",
116 returns: "(uint8 status, uint64 settledValue, uint32 question)",
117 },
118];
119
120#[must_use]
122pub fn spec(at: u16) -> Option<&'static Spec> {
123 ALL.iter().find(|one| one.at == at)
124}
125
126#[must_use]
128pub fn address(at: u16) -> Address {
129 let mut bytes = [0u8; 20];
130 bytes[18..].copy_from_slice(&at.to_be_bytes());
131 Address::from_bytes(bytes)
132}
133
134#[derive(Debug, Clone)]
136pub struct Read<T> {
137 pub at: Address,
138 pub data: Vec<u8>,
139 decode: fn(&[u8]) -> Result<T, AbiError>,
140}
141
142impl<T> Read<T> {
143 #[must_use]
145 pub fn request(&self) -> CallRequest {
146 CallRequest::to(self.at, self.data.clone())
147 }
148
149 pub fn decode(&self, answer: &[u8]) -> Result<T, AbiError> {
151 (self.decode)(answer)
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
156pub enum ReadError {
157 #[error(transparent)]
158 Rpc(#[from] RpcError),
159 #[error("the precompile's answer does not fit its struct: {0}")]
160 Abi(#[from] AbiError),
161}
162
163impl<H: Http> Rpc<H> {
164 pub async fn read<T>(&self, read: &Read<T>) -> Result<T, ReadError> {
166 let answer = self.call(&read.request(), BlockTag::Latest).await?;
167 Ok(read.decode(&answer)?)
168 }
169}
170
171enum Arg {
174 Address(Address),
175 Uint(u64),
176}
177
178fn read<T>(at: u16, args: &[Arg], decode: fn(&[u8]) -> Result<T, AbiError>) -> Read<T> {
179 let mut data = Vec::with_capacity(args.len() * 32);
180 for arg in args {
181 let word = match arg {
182 Arg::Address(address) => B256::from_address(*address).0,
183 Arg::Uint(value) => U256::from_u64(*value).to_be_bytes(),
184 };
185 data.extend_from_slice(&word);
186 }
187 Read { at: address(at), data, decode }
188}
189
190fn tokens(at: u16, answer: &[u8]) -> Result<Vec<Token>, AbiError> {
192 let returns = spec(at).map_or("", |one| one.returns);
193 abi::decode(&abi::params(returns)?, answer)
194}
195
196struct Fields<'a> {
198 name: &'static str,
199 items: std::slice::Iter<'a, Token>,
200}
201
202impl<'a> Fields<'a> {
203 fn of(name: &'static str, tokens: &'a [Token]) -> Result<Fields<'a>, AbiError> {
205 match tokens {
206 [Token::Tuple(items)] => Ok(Fields { name, items: items.iter() }),
207 _ => Err(AbiError::Type(format!("{name}: not one struct"))),
208 }
209 }
210
211 fn take<T>(&mut self, read: impl FnOnce(&'a Token) -> Option<T>) -> Result<T, AbiError> {
212 self.items
213 .next()
214 .and_then(read)
215 .ok_or_else(|| AbiError::Type(format!("{}: a field of another type", self.name)))
216 }
217
218 fn u64(&mut self) -> Result<u64, AbiError> {
219 self.take(Token::as_u64)
220 }
221
222 fn u32(&mut self) -> Result<u32, AbiError> {
223 self.take(Token::as_u32)
224 }
225
226 fn u8(&mut self) -> Result<u8, AbiError> {
227 self.take(Token::as_u8)
228 }
229
230 fn i64(&mut self) -> Result<i64, AbiError> {
231 self.take(Token::as_i64)
232 }
233
234 fn i8(&mut self) -> Result<i8, AbiError> {
235 self.take(Token::as_i8)
236 }
237
238 fn bool(&mut self) -> Result<bool, AbiError> {
239 self.take(Token::as_bool)
240 }
241
242 fn address(&mut self) -> Result<Address, AbiError> {
243 self.take(Token::as_address)
244 }
245
246 fn string(&mut self) -> Result<String, AbiError> {
247 self.take(|token| token.as_str().map(ToOwned::to_owned))
248 }
249
250 fn items(&mut self) -> Result<&'a [Token], AbiError> {
251 self.take(Token::as_items)
252 }
253
254 fn u64s(&mut self) -> Result<Vec<u64>, AbiError> {
255 let name = self.name;
256 self.items()?
257 .iter()
258 .map(|item| item.as_u64().ok_or_else(|| AbiError::Type(format!("{name}: not a list of uint64"))))
259 .collect()
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265pub struct Position {
266 pub szi: i64,
268 pub entry_ntl: u64,
270 pub isolated_raw_usd: i64,
271 pub leverage: u32,
272 pub is_isolated: bool,
273}
274
275fn decode_position(at: u16, answer: &[u8]) -> Result<Position, AbiError> {
276 let tokens = tokens(at, answer)?;
277 let mut fields = Fields::of("position", &tokens)?;
278 Ok(Position {
279 szi: fields.i64()?,
280 entry_ntl: fields.u64()?,
281 isolated_raw_usd: fields.i64()?,
282 leverage: fields.u32()?,
283 is_isolated: fields.bool()?,
284 })
285}
286
287#[must_use]
290pub fn position(user: Address, perp: u16) -> Read<Position> {
291 read(0x800, &[Arg::Address(user), Arg::Uint(u64::from(perp))], |answer| decode_position(0x800, answer))
292}
293
294#[must_use]
297pub fn position2(user: Address, perp: u32) -> Read<Position> {
298 read(0x813, &[Arg::Address(user), Arg::Uint(u64::from(perp))], |answer| decode_position(0x813, answer))
299}
300
301#[derive(Debug, Clone, Copy, PartialEq, Eq)]
303pub struct SpotBalance {
304 pub total: u64,
305 pub hold: u64,
307 pub entry_ntl: u64,
308}
309
310#[must_use]
313pub fn spot_balance(user: Address, token: u64) -> Read<SpotBalance> {
314 read(0x801, &[Arg::Address(user), Arg::Uint(token)], |answer| {
315 let tokens = tokens(0x801, answer)?;
316 let mut fields = Fields::of("spotBalance", &tokens)?;
317 Ok(SpotBalance { total: fields.u64()?, hold: fields.u64()?, entry_ntl: fields.u64()? })
318 })
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
322pub struct UserVaultEquity {
323 pub equity: u64,
325 pub locked_until_timestamp: u64,
327}
328
329#[must_use]
331pub fn user_vault_equity(user: Address, vault: Address) -> Read<UserVaultEquity> {
332 read(0x802, &[Arg::Address(user), Arg::Address(vault)], |answer| {
333 let tokens = tokens(0x802, answer)?;
334 let mut fields = Fields::of("userVaultEquity", &tokens)?;
335 Ok(UserVaultEquity { equity: fields.u64()?, locked_until_timestamp: fields.u64()? })
336 })
337}
338
339#[must_use]
341pub fn withdrawable(user: Address) -> Read<u64> {
342 read(0x803, &[Arg::Address(user)], |answer| Fields::of("withdrawable", &tokens(0x803, answer)?)?.u64())
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
347pub struct Delegation {
348 pub validator: Address,
349 pub amount: u64,
351 pub locked_until_timestamp: u64,
353}
354
355#[must_use]
357pub fn delegations(user: Address) -> Read<Vec<Delegation>> {
358 read(0x804, &[Arg::Address(user)], |answer| {
359 let tokens = tokens(0x804, answer)?;
360 let [Token::Array(items)] = tokens.as_slice() else {
361 return Err(AbiError::Type("delegations: not a list".to_owned()));
362 };
363 items
364 .iter()
365 .map(|item| {
366 let mut fields = Fields::of("delegations", std::slice::from_ref(item))?;
367 Ok(Delegation {
368 validator: fields.address()?,
369 amount: fields.u64()?,
370 locked_until_timestamp: fields.u64()?,
371 })
372 })
373 .collect()
374 })
375}
376
377#[derive(Debug, Clone, Copy, PartialEq, Eq)]
379pub struct DelegatorSummary {
380 pub delegated: u64,
381 pub undelegated: u64,
382 pub total_pending_withdrawal: u64,
383 pub n_pending_withdrawals: u64,
384}
385
386#[must_use]
388pub fn delegator_summary(user: Address) -> Read<DelegatorSummary> {
389 read(0x805, &[Arg::Address(user)], |answer| {
390 let tokens = tokens(0x805, answer)?;
391 let mut fields = Fields::of("delegatorSummary", &tokens)?;
392 Ok(DelegatorSummary {
393 delegated: fields.u64()?,
394 undelegated: fields.u64()?,
395 total_pending_withdrawal: fields.u64()?,
396 n_pending_withdrawals: fields.u64()?,
397 })
398 })
399}
400
401fn bare(at: u16, answer: &[u8]) -> Result<u64, AbiError> {
402 match tokens(at, answer)?.as_slice() {
403 [one] => one.as_u64().ok_or_else(|| AbiError::Type("not a uint64".to_owned())),
404 _ => Err(AbiError::Type("not one uint64".to_owned())),
405 }
406}
407
408#[must_use]
410pub fn mark_px(index: u32) -> Read<u64> {
411 read(0x806, &[Arg::Uint(u64::from(index))], |answer| bare(0x806, answer))
412}
413
414#[must_use]
416pub fn oracle_px(index: u32) -> Read<u64> {
417 read(0x807, &[Arg::Uint(u64::from(index))], |answer| bare(0x807, answer))
418}
419
420#[must_use]
424pub fn spot_px(index: u32) -> Read<u64> {
425 read(0x808, &[Arg::Uint(u64::from(index))], |answer| bare(0x808, answer))
426}
427
428#[must_use]
430pub fn l1_block_number() -> Read<u64> {
431 read(0x809, &[], |answer| bare(0x809, answer))
432}
433
434#[derive(Debug, Clone, PartialEq, Eq)]
435pub struct PerpAssetInfo {
436 pub coin: String,
437 pub margin_table_id: u32,
438 pub sz_decimals: u8,
439 pub max_leverage: u8,
440 pub only_isolated: bool,
441}
442
443#[must_use]
445pub fn perp_asset_info(perp: u32) -> Read<PerpAssetInfo> {
446 read(0x80a, &[Arg::Uint(u64::from(perp))], |answer| {
447 let tokens = tokens(0x80a, answer)?;
448 let mut fields = Fields::of("perpAssetInfo", &tokens)?;
449 Ok(PerpAssetInfo {
450 coin: fields.string()?,
451 margin_table_id: fields.u32()?,
452 sz_decimals: fields.u8()?,
453 max_leverage: fields.u8()?,
454 only_isolated: fields.bool()?,
455 })
456 })
457}
458
459#[derive(Debug, Clone, PartialEq, Eq)]
460pub struct SpotInfo {
461 pub name: String,
462 pub tokens: [u64; 2],
464}
465
466#[must_use]
468pub fn spot_info(spot: u32) -> Read<SpotInfo> {
469 read(0x80b, &[Arg::Uint(u64::from(spot))], |answer| {
470 let tokens = tokens(0x80b, answer)?;
471 let mut fields = Fields::of("spotInfo", &tokens)?;
472 let name = fields.string()?;
473 let pair: [u64; 2] =
474 fields.u64s()?.try_into().map_err(|_| AbiError::Type("spotInfo: not two tokens".to_owned()))?;
475 Ok(SpotInfo { name, tokens: pair })
476 })
477}
478
479#[derive(Debug, Clone, PartialEq, Eq)]
480pub struct TokenInfo {
481 pub name: String,
482 pub spots: Vec<u64>,
484 pub deployer_trading_fee_share: u64,
485 pub deployer: Address,
486 pub evm_contract: Address,
488 pub sz_decimals: u8,
489 pub wei_decimals: u8,
490 pub evm_extra_wei_decimals: i8,
492}
493
494#[must_use]
496pub fn token_info(token: u32) -> Read<TokenInfo> {
497 read(0x80c, &[Arg::Uint(u64::from(token))], |answer| {
498 let tokens = tokens(0x80c, answer)?;
499 let mut fields = Fields::of("tokenInfo", &tokens)?;
500 Ok(TokenInfo {
501 name: fields.string()?,
502 spots: fields.u64s()?,
503 deployer_trading_fee_share: fields.u64()?,
504 deployer: fields.address()?,
505 evm_contract: fields.address()?,
506 sz_decimals: fields.u8()?,
507 wei_decimals: fields.u8()?,
508 evm_extra_wei_decimals: fields.i8()?,
509 })
510 })
511}
512
513#[derive(Debug, Clone, Copy, PartialEq, Eq)]
515pub struct UserBalance {
516 pub user: Address,
517 pub balance: u64,
518}
519
520#[derive(Debug, Clone, PartialEq, Eq)]
521pub struct TokenSupply {
522 pub max_supply: u64,
523 pub total_supply: u64,
524 pub circulating_supply: u64,
525 pub future_emissions: u64,
526 pub non_circulating_user_balances: Vec<UserBalance>,
527}
528
529#[must_use]
531pub fn token_supply(token: u32) -> Read<TokenSupply> {
532 read(0x80d, &[Arg::Uint(u64::from(token))], |answer| {
533 let tokens = tokens(0x80d, answer)?;
534 let mut fields = Fields::of("tokenSupply", &tokens)?;
535 let (max_supply, total_supply, circulating_supply, future_emissions) =
536 (fields.u64()?, fields.u64()?, fields.u64()?, fields.u64()?);
537 let holders = fields
538 .items()?
539 .iter()
540 .map(|item| {
541 let mut holder = Fields::of("tokenSupply", std::slice::from_ref(item))?;
542 Ok(UserBalance { user: holder.address()?, balance: holder.u64()? })
543 })
544 .collect::<Result<_, AbiError>>()?;
545 Ok(TokenSupply {
546 max_supply,
547 total_supply,
548 circulating_supply,
549 future_emissions,
550 non_circulating_user_balances: holders,
551 })
552 })
553}
554
555#[derive(Debug, Clone, Copy, PartialEq, Eq)]
557pub struct Bbo {
558 pub bid: u64,
559 pub ask: u64,
560}
561
562#[must_use]
565pub fn bbo(asset: u32) -> Read<Bbo> {
566 read(0x80e, &[Arg::Uint(u64::from(asset))], |answer| {
567 let tokens = tokens(0x80e, answer)?;
568 let mut fields = Fields::of("bbo", &tokens)?;
569 Ok(Bbo { bid: fields.u64()?, ask: fields.u64()? })
570 })
571}
572
573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
575pub struct AccountMarginSummary {
576 pub account_value: i64,
577 pub margin_used: u64,
578 pub ntl_pos: u64,
579 pub raw_usd: i64,
580}
581
582#[must_use]
584pub fn account_margin_summary(perp_dex_index: u32, user: Address) -> Read<AccountMarginSummary> {
585 read(0x80f, &[Arg::Uint(u64::from(perp_dex_index)), Arg::Address(user)], |answer| {
586 let tokens = tokens(0x80f, answer)?;
587 let mut fields = Fields::of("accountMarginSummary", &tokens)?;
588 Ok(AccountMarginSummary {
589 account_value: fields.i64()?,
590 margin_used: fields.u64()?,
591 ntl_pos: fields.u64()?,
592 raw_usd: fields.i64()?,
593 })
594 })
595}
596
597#[must_use]
599pub fn core_user_exists(user: Address) -> Read<bool> {
600 read(0x810, &[Arg::Address(user)], |answer| Fields::of("coreUserExists", &tokens(0x810, answer)?)?.bool())
601}
602
603#[derive(Debug, Clone, Copy, PartialEq, Eq)]
604pub struct BasisAndValue {
605 pub basis: u64,
606 pub value: u64,
607}
608
609#[derive(Debug, Clone, Copy, PartialEq, Eq)]
610pub struct BorrowLendUserState {
611 pub borrow: BasisAndValue,
612 pub supply: BasisAndValue,
613}
614
615#[must_use]
618pub fn borrow_lend_user_state(user: Address, token: u64) -> Read<BorrowLendUserState> {
619 read(0x811, &[Arg::Address(user), Arg::Uint(token)], |answer| {
620 let tokens = tokens(0x811, answer)?;
621 let mut fields = Fields::of("borrowLendUserState", &tokens)?;
622 let pair = |fields: &mut Fields<'_>| -> Result<BasisAndValue, AbiError> {
623 let item = fields.items.next().ok_or_else(|| AbiError::Type("borrowLendUserState: short".to_owned()))?;
624 let mut inner = Fields::of("borrowLendUserState", std::slice::from_ref(item))?;
625 Ok(BasisAndValue { basis: inner.u64()?, value: inner.u64()? })
626 };
627 Ok(BorrowLendUserState { borrow: pair(&mut fields)?, supply: pair(&mut fields)? })
628 })
629}
630
631#[derive(Debug, Clone, Copy, PartialEq, Eq)]
632pub struct BorrowLendReserveState {
633 pub borrow_yearly_rate_bps: u64,
634 pub supply_yearly_rate_bps: u64,
635 pub balance: u64,
636 pub utilization_bps: u64,
637 pub oracle_px: u64,
638 pub ltv_bps: u64,
639 pub total_supplied: u64,
640 pub total_borrowed: u64,
641}
642
643#[must_use]
646pub fn borrow_lend_reserve_state(token: u64) -> Read<BorrowLendReserveState> {
647 read(0x812, &[Arg::Uint(token)], |answer| {
648 let tokens = tokens(0x812, answer)?;
649 let mut fields = Fields::of("borrowLendReserveState", &tokens)?;
650 Ok(BorrowLendReserveState {
651 borrow_yearly_rate_bps: fields.u64()?,
652 supply_yearly_rate_bps: fields.u64()?,
653 balance: fields.u64()?,
654 utilization_bps: fields.u64()?,
655 oracle_px: fields.u64()?,
656 ltv_bps: fields.u64()?,
657 total_supplied: fields.u64()?,
658 total_borrowed: fields.u64()?,
659 })
660 })
661}
662
663#[derive(Debug, Clone, Copy, PartialEq, Eq)]
665pub struct OutcomeStatus {
666 pub status: u8,
667 pub settled_value: u64,
669 pub question: u32,
671}
672
673#[derive(Debug, Clone, Copy, PartialEq, Eq)]
675pub enum OutcomeState {
676 NeverExisted,
677 Active,
678 Settled,
680 Pruned,
682 Other(u8),
683}
684
685pub const SETTLED_DECIMALS: u32 = 8;
687
688impl OutcomeStatus {
689 #[must_use]
690 pub const fn state(&self) -> OutcomeState {
691 match self.status {
692 0 => OutcomeState::NeverExisted,
693 1 => OutcomeState::Active,
694 2 => OutcomeState::Settled,
695 3 => OutcomeState::Pruned,
696 other => OutcomeState::Other(other),
697 }
698 }
699
700 #[must_use]
702 pub const fn in_question(&self) -> Option<u32> {
703 match self.question {
704 u32::MAX => None,
705 question => Some(question),
706 }
707 }
708
709 #[must_use]
711 pub fn settled(&self) -> Option<Dec> {
712 match self.state() {
713 OutcomeState::Settled => Dec::new(i128::from(self.settled_value), SETTLED_DECIMALS),
714 _ => None,
715 }
716 }
717}
718
719#[must_use]
721pub fn outcome_status(outcome: u32) -> Read<OutcomeStatus> {
722 read(0x814, &[Arg::Uint(u64::from(outcome))], |answer| {
723 let tokens = tokens(0x814, answer)?;
724 let mut fields = Fields::of("outcomeStatus", &tokens)?;
725 Ok(OutcomeStatus { status: fields.u8()?, settled_value: fields.u64()?, question: fields.u32()? })
726 })
727}
728
729#[must_use]
731pub fn perp_price(raw: u64, sz_decimals: u8) -> Option<Dec> {
732 Dec::new(i128::from(raw), 6u32.checked_sub(u32::from(sz_decimals))?)
733}
734
735#[must_use]
737pub fn spot_price(raw: u64, base_sz_decimals: u8) -> Option<Dec> {
738 Dec::new(i128::from(raw), 8u32.checked_sub(u32::from(base_sz_decimals))?)
739}
740
741#[cfg(test)]
742#[path = "tests/precompiles.rs"]
743mod tests;