Skip to main content

df_hyperliquid_core/client/evm/
words.rs

1//! CoreWriter actions in words: an asset id becomes its market, a token index
2//! its name, and every amount is scaled by its unit ([`crate::client::evm::corewriter`]).
3
4use std::collections::BTreeMap;
5
6use crate::client::evm::corewriter::{
7    Abstraction, Action, BorrowLendOp, DECIBPS_DECIMALS, FinalizeVariant, HYPE_DECIMALS, ORDER_DECIMALS, OutcomeOp,
8    SPOT_DEX, Tif, USD_DECIMALS,
9};
10use crate::utils::{Address, Dec};
11
12/// What a network calls its markets, tokens and dexes, to put names to ids.
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct Names {
15    /// By the asset id an order names a market by.
16    pub markets: BTreeMap<u32, Market>,
17    /// By token index.
18    pub tokens: BTreeMap<u64, TokenName>,
19    /// The HIP-3 dexes in `perpDexs` order, the main dex left out.
20    pub dexes: Vec<String>,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Market {
25    pub name: String,
26    pub spot: bool,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct TokenName {
31    pub name: String,
32    pub wei_decimals: u32,
33}
34
35impl Names {
36    /// The market an asset id names, or `asset <id>`.
37    #[must_use]
38    pub fn market(&self, asset: u32) -> String {
39        self.markets.get(&asset).map_or_else(|| format!("asset {asset}"), |market| market.name.clone())
40    }
41
42    /// A token's name, or `token <index>`.
43    #[must_use]
44    pub fn token(&self, index: u64) -> String {
45        self.tokens.get(&index).map_or_else(|| format!("token {index}"), |token| token.name.clone())
46    }
47
48    /// An amount of a token in its wei, scaled by its decimals when they are known.
49    #[must_use]
50    pub fn amount(&self, index: u64, wei: u64) -> String {
51        match self.tokens.get(&index) {
52            Some(token) => format!("{} {}", scaled(u128::from(wei), token.wei_decimals), token.name),
53            None => format!("{wei} wei of token {index}"),
54        }
55    }
56
57    /// A dex as `sendAsset` numbers it: spot, the main perp dex, or a HIP-3 one.
58    #[must_use]
59    pub fn dex(&self, dex: u32) -> String {
60        match dex {
61            SPOT_DEX => "spot".to_owned(),
62            0 => "perp".to_owned(),
63            at => usize::try_from(at - 1)
64                .ok()
65                .and_then(|at| self.dexes.get(at))
66                .map_or_else(|| format!("dex {dex}"), |name| format!("{name} perp")),
67        }
68    }
69}
70
71/// `units` at `decimals` places, or the units as they are past what a [`Dec`] keeps.
72fn scaled(units: u128, decimals: u32) -> String {
73    i128::try_from(units)
74        .ok()
75        .and_then(|units| Dec::new(units, decimals))
76        .map_or_else(|| units.to_string(), |dec| dec.to_string())
77}
78
79/// An address cut to its ends: `0x7d1c..fe05`.
80#[must_use]
81pub fn short(address: Address) -> String {
82    let text = address.to_hex();
83    format!("{}..{}", &text[..6], &text[text.len() - 4..])
84}
85
86fn hype(wei: u64) -> String {
87    format!("{} HYPE", scaled(u128::from(wei), HYPE_DECIMALS))
88}
89
90fn usd(units: u64) -> String {
91    format!("{} USDC", scaled(u128::from(units), USD_DECIMALS))
92}
93
94fn tif(tif: Tif) -> String {
95    match tif {
96        Tif::Alo => "Alo".to_owned(),
97        Tif::Gtc => "Gtc".to_owned(),
98        Tif::Ioc => "Ioc".to_owned(),
99        Tif::Other(code) => format!("encodedTif {code}"),
100    }
101}
102
103fn variant(variant: FinalizeVariant) -> String {
104    match variant {
105        FinalizeVariant::Create => "Create".to_owned(),
106        FinalizeVariant::FirstStorageSlot => "FirstStorageSlot".to_owned(),
107        FinalizeVariant::CustomStorageSlot => "CustomStorageSlot".to_owned(),
108        FinalizeVariant::Other(code) => format!("encodedFinalizeEvmContractVariant {code}"),
109    }
110}
111
112fn lending(operation: BorrowLendOp) -> String {
113    match operation {
114        BorrowLendOp::Supply => "Supply".to_owned(),
115        BorrowLendOp::Withdraw => "Withdraw".to_owned(),
116        BorrowLendOp::Borrow => "Borrow (undocumented)".to_owned(),
117        BorrowLendOp::Repay => "Repay (undocumented)".to_owned(),
118        BorrowLendOp::Other(code) => format!("encodedOperation {code}"),
119    }
120}
121
122fn mode(abstraction: Abstraction) -> String {
123    match abstraction {
124        Abstraction::Disabled => "disabled".to_owned(),
125        Abstraction::UnifiedAccount => "unifiedAccount".to_owned(),
126        Abstraction::PortfolioMargin => "portfolioMargin".to_owned(),
127        Abstraction::Other(code) => format!("abstraction {code}"),
128    }
129}
130
131fn outcome(operation: OutcomeOp) -> String {
132    match operation {
133        OutcomeOp::SplitOutcome => "SplitOutcome".to_owned(),
134        OutcomeOp::MergeOutcome => "MergeOutcome".to_owned(),
135        OutcomeOp::MergeQuestion => "MergeQuestion".to_owned(),
136        OutcomeOp::NegateOutcome => "NegateOutcome".to_owned(),
137        OutcomeOp::Other(code) => format!("encodedOperation {code}"),
138    }
139}
140
141impl Action {
142    /// The action on one line, as a person would say it.
143    #[must_use]
144    pub fn describe(&self, names: &Names) -> String {
145        match self {
146            Action::LimitOrder { asset, is_buy, limit_px, sz, reduce_only, tif: time, .. } => {
147                let side = if *is_buy { "buy" } else { "sell" };
148                let mut said = format!(
149                    "{side} {} {} at {}",
150                    scaled(u128::from(*sz), ORDER_DECIMALS),
151                    names.market(*asset),
152                    scaled(u128::from(*limit_px), ORDER_DECIMALS)
153                );
154                if *reduce_only {
155                    said.push_str("  reduce only");
156                }
157                format!("{said}  {}", tif(*time))
158            }
159            Action::VaultTransfer { vault, is_deposit: true, usd: amount } => {
160                format!("deposit {} into {}", usd(*amount), short(*vault))
161            }
162            Action::VaultTransfer { vault, is_deposit: false, usd: amount } => {
163                format!("withdraw {} from {}", usd(*amount), short(*vault))
164            }
165            Action::TokenDelegate { validator, wei, is_undelegate: false } => {
166                format!("delegate {} to {}", hype(*wei), short(*validator))
167            }
168            Action::TokenDelegate { validator, wei, is_undelegate: true } => {
169                format!("undelegate {} from {}", hype(*wei), short(*validator))
170            }
171            Action::StakingDeposit { wei } => format!("{} into staking", hype(*wei)),
172            Action::StakingWithdraw { wei } => format!("{} out of staking", hype(*wei)),
173            Action::SpotSend { destination, token, wei } => {
174                format!("{} to {}", names.amount(*token, *wei), short(*destination))
175            }
176            Action::UsdClassTransfer { ntl, to_perp } => {
177                format!("{}  {}", usd(*ntl), if *to_perp { "spot to perp" } else { "perp to spot" })
178            }
179            Action::FinalizeEvmContract { token, variant: how, create_nonce } => {
180                format!("{}  {}  nonce {create_nonce}", names.token(*token), variant(*how))
181            }
182            Action::AddApiWallet { wallet, name } => match name.as_str() {
183                "" => format!("{}  unnamed", short(*wallet)),
184                name => format!("{}  named {name}", short(*wallet)),
185            },
186            Action::CancelByOid { asset, oid } => format!("{}  oid {oid}", names.market(*asset)),
187            Action::CancelByCloid { asset, cloid } => format!("{}  cloid 0x{cloid:032x}", names.market(*asset)),
188            Action::ApproveBuilderFee { max_fee_rate, builder } => {
189                format!("up to {}% for {}", scaled(u128::from(*max_fee_rate), DECIBPS_DECIMALS), short(*builder))
190            }
191            Action::SendAsset { destination, sub_account, source_dex, destination_dex, token, wei } => {
192                let mut said = format!(
193                    "{} to {}  {} to {}",
194                    names.amount(*token, *wei),
195                    short(*destination),
196                    names.dex(*source_dex),
197                    names.dex(*destination_dex)
198                );
199                if !sub_account.is_zero() {
200                    said.push_str(&format!("  from sub {}", short(*sub_account)));
201                }
202                said
203            }
204            Action::BorrowLend { operation, token, wei } => match wei {
205                0 => format!("{} the most there is of {}", lending(*operation), names.token(*token)),
206                _ => format!("{} {}", lending(*operation), names.amount(*token, *wei)),
207            },
208            Action::SetAbstraction { user, abstraction } => format!("{} for {}", mode(*abstraction), short(*user)),
209            Action::Outcome { operation, question, outcome: id, wei } => {
210                let amount = match wei {
211                    0 => "the most there is".to_owned(),
212                    _ => format!("{wei} wei"),
213                };
214                match operation {
215                    OutcomeOp::MergeQuestion => format!("{}  question {question}  {amount}", outcome(*operation)),
216                    OutcomeOp::NegateOutcome => {
217                        format!("{}  question {question} outcome {id}  {amount}", outcome(*operation))
218                    }
219                    _ => format!("{}  outcome {id}  {amount}", outcome(*operation)),
220                }
221            }
222        }
223    }
224}
225
226#[cfg(test)]
227#[path = "tests/words.rs"]
228mod tests;