1use 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct Names {
15 pub markets: BTreeMap<u32, Market>,
17 pub tokens: BTreeMap<u64, TokenName>,
19 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 #[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 #[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 #[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 #[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
71fn 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#[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 #[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;