Skip to main content

df_hyperliquid_core/client/evm/
precompiles.rs

1//! HyperCore's read precompiles: HyperCore state read from HyperEVM by an
2//! `eth_call` to `0x...0800` through `0x...0814`, as `L1Read.sol` in
3//! Hyperliquid's docs declares them.
4//!
5//! **The calldata is the ABI-encoded arguments with no selector.**
6//! `L1Read.sol` calls each as `ADDRESS.staticcall(abi.encode(...))`, so a
7//! selector would be read as part of the first argument.
8//!
9//! The numbers are HyperCore's integers, unscaled:
10//! - a perp's `markPx`, `oraclePx` and `bbo` are in `10^-(6 - szDecimals)`
11//!   ([`perp_price`]), a spot pair's `spotPx` and `bbo` in
12//!   `10^-(8 - szDecimals)` of its base token ([`spot_price`]);
13//! - a position's `szi` is in the perp's size decimals;
14//! - USDC amounts (`entryNtl`, `equity`, `withdrawable`, margins) are 1e6;
15//! - token balances are in the token's `weiDecimals`, HYPE staking in 1e8;
16//! - an outcome's `settledValue` is 1e8 for 1.0.
17//!
18//! An invalid input (an unknown asset or vault) is refused and uses all the
19//! gas given. Gas is `2000 + 65 * (input + output bytes)`.
20
21use 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/// One precompile as `L1Read.sol` declares it.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct Spec {
30    /// The low two bytes of its address.
31    pub at: u16,
32    /// The `L1Read.sol` function that wraps it.
33    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
40/// Every read precompile `L1Read.sol` names, in address order.
41pub 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/// The precompile at `0x...<at>`, if `L1Read.sol` names one there.
121#[must_use]
122pub fn spec(at: u16) -> Option<&'static Spec> {
123    ALL.iter().find(|one| one.at == at)
124}
125
126/// `0x` and nineteen zero bytes, then the two of `at`.
127#[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/// A typed read: where the call goes, its calldata, and how its answer reads.
135#[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    /// The `eth_call` that makes the read.
144    #[must_use]
145    pub fn request(&self) -> CallRequest {
146        CallRequest::to(self.at, self.data.clone())
147    }
148
149    /// The answer's bytes, read against the declared struct.
150    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    /// One read precompile, at the latest block: the only one HyperEVM serves.
165    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
171/// One argument word: every precompile argument is a static type, so the
172/// calldata is the words one after another, as `abi.encode` writes them.
173enum 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
190/// The answer of the precompile at `at`, decoded against its declared return.
191fn 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
196/// The fields of a struct answer, read in order.
197struct Fields<'a> {
198    name: &'static str,
199    items: std::slice::Iter<'a, Token>,
200}
201
202impl<'a> Fields<'a> {
203    /// The fields of the one tuple an answer is.
204    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/// One perp position, as [`position`] and [`position2`] answer it.
264#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265pub struct Position {
266    /// Signed size in the perp's size decimals; negative is short.
267    pub szi: i64,
268    /// Entry notional, USDC at 1e6.
269    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/// `0x800`: one account's position in a perp of the main dex, by a `uint16`
288/// index. [`position2`] takes a `uint32`.
289#[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/// `0x813`: [`position`] with a `uint32` perp index, a new address rather than
295/// a change to the old one.
296#[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/// One token's balance on HyperCore's spot side, in its `weiDecimals`.
302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
303pub struct SpotBalance {
304    pub total: u64,
305    /// What open orders hold of it.
306    pub hold: u64,
307    pub entry_ntl: u64,
308}
309
310/// `0x801`: a spot balance. `token` is a token index, or an outcome side's
311/// asset id, `100000000 + outcome * 10 + side`.
312#[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    /// USDC at 1e6.
324    pub equity: u64,
325    /// Unix milliseconds.
326    pub locked_until_timestamp: u64,
327}
328
329/// `0x802`: an account's equity in a vault, and when it unlocks.
330#[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/// `0x803`: what the account can withdraw now, USDC at 1e6.
340#[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/// One stake delegation.
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
347pub struct Delegation {
348    pub validator: Address,
349    /// HYPE at 1e8.
350    pub amount: u64,
351    /// Unix milliseconds.
352    pub locked_until_timestamp: u64,
353}
354
355/// `0x804`: every delegation the account holds.
356#[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/// Staking totals, HYPE at 1e8.
378#[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/// `0x805`: the account's staking totals.
387#[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/// `0x806`: a perp's mark price, in `10^-(6 - szDecimals)` ([`perp_price`]).
409#[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/// `0x807`: a perp's oracle price, scaled as [`mark_px`].
415#[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/// `0x808`: a spot pair's price, in `10^-(8 - szDecimals)` of its base token
421/// ([`spot_price`]). `index` is a pair index, or an outcome side's asset id,
422/// `100000000 + outcome * 10 + side`.
423#[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/// `0x809`: HyperCore's block height. The one precompile with no arguments.
429#[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/// `0x80a`: a perp's name, size decimals and leverage cap.
444#[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    /// The base token's index, then the quote's.
463    pub tokens: [u64; 2],
464}
465
466/// `0x80b`: a spot pair and the two tokens it is between.
467#[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    /// The spot pairs it trades in.
483    pub spots: Vec<u64>,
484    pub deployer_trading_fee_share: u64,
485    pub deployer: Address,
486    /// The linked ERC-20 on HyperEVM; zero when it has none.
487    pub evm_contract: Address,
488    pub sz_decimals: u8,
489    pub wei_decimals: u8,
490    /// How many more decimals the ERC-20 counts in than HyperCore does.
491    pub evm_extra_wei_decimals: i8,
492}
493
494/// `0x80c`: everything about a token, its EVM contract included.
495#[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/// An account holding non-circulating supply.
514#[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/// `0x80d`: a token's supply, and who holds the part that does not circulate.
530#[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/// The best bid and offer, scaled as the market's prices are.
556#[derive(Debug, Clone, Copy, PartialEq, Eq)]
557pub struct Bbo {
558    pub bid: u64,
559    pub ask: u64,
560}
561
562/// `0x80e`: the best bid and offer of a perp (its index) or a spot pair
563/// (10000 plus its index).
564#[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/// An account's margin on one perp dex, USDC at 1e6.
574#[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/// `0x80f`: margin on a perp dex, 0 the main one.
583#[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/// `0x810`: whether the address exists on HyperCore at all.
598#[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/// `0x811`: one account's borrow and supply in a token. The precompile
616/// refuses an account that has never lent or borrowed it.
617#[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/// `0x812`: a token's lending pool. Its token is a `uint64`, where most
644/// neighbours take a `uint32`.
645#[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/// A HIP-4 outcome as the chain sees it, the only HIP-4 read on HyperEVM.
664#[derive(Debug, Clone, Copy, PartialEq, Eq)]
665pub struct OutcomeStatus {
666    pub status: u8,
667    /// 1e8 is 1.0; meaningful once settled.
668    pub settled_value: u64,
669    /// `u32::MAX` when the outcome stands alone or its question is unknown.
670    pub question: u32,
671}
672
673/// What [`OutcomeStatus::status`] says.
674#[derive(Debug, Clone, Copy, PartialEq, Eq)]
675pub enum OutcomeState {
676    NeverExisted,
677    Active,
678    /// Settled, its value readable.
679    Settled,
680    /// Settled and pruned: its value is gone.
681    Pruned,
682    Other(u8),
683}
684
685/// The places [`OutcomeStatus::settled_value`] is counted in.
686pub 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    /// The question the outcome belongs to, if any.
701    #[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    /// The settlement, 0 to 1, once settled and not pruned.
710    #[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/// `0x814`: an outcome's status.
720#[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/// A perp price from [`mark_px`], [`oracle_px`] or [`bbo`]: `raw / 10^(6 - szDecimals)`.
730#[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/// A spot price from [`spot_px`] or [`bbo`]: `raw / 10^(8 - szDecimals)` of the base token.
736#[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;