levana_perpswap_cosmos/
token.rs

1//! Represents the native coin or CW20 used for collateral in a market.
2use crate::contracts::{
3    cw20::entry::{
4        BalanceResponse as Cw20BalanceResponse, ExecuteMsg as Cw20ExecuteMsg,
5        QueryMsg as Cw20QueryMsg,
6    },
7    market::entry::ExecuteMsg as MarketExecuteMsg,
8};
9use crate::prelude::*;
10
11use cosmwasm_std::{
12    to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Decimal256, QuerierWrapper, WasmMsg,
13};
14use serde::Serialize;
15
16/// The number of decimal places for tokens may vary
17/// and there is a smart query cost for deriving it at runtime
18/// so we grab the info at init time and then store it as a full-fledged token
19#[cw_serde]
20pub enum TokenInit {
21    /// A cw20 address. Decimal places will be derived.
22    Cw20 {
23        /// Address of the CW20 contract
24        addr: RawAddr,
25    },
26
27    /// Native currency. May cover some IBC tokens too
28    Native {
29        /// Denom used within the chain for this native coin
30        denom: String,
31        /// Number of decimal points
32        decimal_places: u8,
33    },
34}
35
36impl From<Token> for TokenInit {
37    fn from(src: Token) -> Self {
38        match src {
39            Token::Native {
40                denom,
41                decimal_places,
42            } => Self::Native {
43                denom,
44                decimal_places,
45            },
46            Token::Cw20 { addr, .. } => Self::Cw20 { addr },
47        }
48    }
49}
50
51/// The overall ideas of the Token API are:
52/// 1. use the Number type, not u128 or Uint128
53/// 2. abstract over the Cw20/Native variants
54///
55/// At the end of the day, call transfer/query with
56/// the same business logic as contract math
57/// and don't worry at all about conversions or addresses/denoms
58#[cw_serde]
59#[derive(Eq)]
60pub enum Token {
61    /// An asset controlled by a CW20 token.
62    Cw20 {
63        /// Address of the contract
64        addr: RawAddr,
65        /// Decimals places used by the contract
66        decimal_places: u8,
67    },
68
69    /// Native coin on the blockchain
70    Native {
71        /// Native coin denom string
72        denom: String,
73        /// Decimal places used by the asset
74        decimal_places: u8,
75    },
76}
77
78impl Token {
79    pub(crate) fn name(&self) -> String {
80        match self {
81            Self::Native { denom, .. } => {
82                format!("native-{}", denom)
83            }
84            Self::Cw20 { addr, .. } => {
85                format!("cw20-{}", addr)
86            }
87        }
88    }
89
90    pub(crate) fn decimal_places(&self) -> u8 {
91        match self {
92            Self::Native { decimal_places, .. } => *decimal_places,
93            Self::Cw20 { decimal_places, .. } => *decimal_places,
94        }
95    }
96    /// This is the usual function to call for transferring money
97    /// the result can simply be added as a Message to any Response
98    /// the amount is expressed as Number such that it mirrors self.query_balance()
99    pub fn into_transfer_msg(
100        &self,
101        recipient: &Addr,
102        amount: NonZero<Collateral>,
103    ) -> Result<Option<CosmosMsg>> {
104        match self {
105            Self::Native { .. } => {
106                let coin = self.into_native_coin(amount.into_number_gt_zero())?;
107
108                match coin {
109                    Some(coin) => Ok(Some(CosmosMsg::Bank(BankMsg::Send {
110                        to_address: recipient.to_string(),
111                        amount: vec![coin],
112                    }))),
113
114                    None => Ok(None),
115                }
116            }
117            Self::Cw20 { addr, .. } => {
118                let msg = self.into_cw20_execute_transfer_msg(recipient, amount)?;
119
120                match msg {
121                    Some(msg) => {
122                        let msg = to_json_binary(&msg)?;
123
124                        Ok(Some(CosmosMsg::Wasm(WasmMsg::Execute {
125                            contract_addr: addr.to_string(),
126                            msg,
127                            funds: Vec::new(),
128                        })))
129                    }
130                    None => Ok(None),
131                }
132            }
133        }
134    }
135
136    /// Get the balance - this is expressed a Collateral
137    /// such that it mirrors self.into_transfer_msg()
138    pub fn query_balance(&self, querier: &QuerierWrapper, user_addr: &Addr) -> Result<Collateral> {
139        self.query_balance_dec(querier, user_addr)
140            .map(Collateral::from_decimal256)
141    }
142
143    /// Get the balance - this is expressed as Decimal256
144    /// such that it mirrors self.into_transfer_msg()
145    pub fn query_balance_dec(
146        &self,
147        querier: &QuerierWrapper,
148        user_addr: &Addr,
149    ) -> Result<Decimal256> {
150        self.from_u128(match self {
151            Self::Cw20 { addr, .. } => {
152                let resp: Cw20BalanceResponse = querier.query_wasm_smart(
153                    addr.as_str(),
154                    &Cw20QueryMsg::Balance {
155                        address: user_addr.to_string().into(),
156                    },
157                )?;
158
159                resp.balance.u128()
160            }
161            Self::Native { denom, .. } => {
162                let coin = querier.query_balance(user_addr, denom)?;
163                coin.amount.u128()
164            }
165        })
166    }
167
168    /// helper function
169    ///
170    /// given a u128, typically via a native Coin.amount or Cw20 amount
171    /// get the Decimal256 representation according to the WalletSource's config
172    ///
173    /// this is essentially the inverse of self.into_u128()
174    pub fn from_u128(&self, amount: u128) -> Result<Decimal256> {
175        Decimal256::from_atomics(amount, self.decimal_places().into()).map_err(|e| e.into())
176    }
177
178    /// helper function
179    ///
180    /// given a number, typically via business logic and client API
181    /// get the u128 representation, e.g. for Coin or Cw20
182    /// according to the WalletSource's config
183    ///
184    /// this will only return None if the amount is zero (or rounds to 0)
185    /// which then bubbles up into other methods that build on this
186    ///
187    /// this is essentially the inverse of self.from_u128()
188    pub fn into_u128(&self, amount: Decimal256) -> Result<Option<u128>> {
189        let value: u128 = amount
190            .into_number()
191            .to_u128_with_precision(self.decimal_places().into())
192            .ok_or_else(|| {
193                anyhow!(PerpError::new(
194                    ErrorId::Conversion,
195                    ErrorDomain::Wallet,
196                    format!("{} unable to convert {amount} to u128!", self.name(),)
197                ))
198            })?;
199
200        if value > 0 {
201            Ok(Some(value))
202        } else {
203            Ok(None)
204        }
205    }
206
207    /// helper function
208    ///
209    /// when we know for a fact we have a WalletSource::native
210    /// we can get a Coin from a Number amount
211    pub fn into_native_coin(&self, amount: NumberGtZero) -> Result<Option<Coin>> {
212        match self {
213            Self::Native { denom, .. } => {
214                Ok(self
215                    .into_u128(amount.into_decimal256())?
216                    .map(|amount| Coin {
217                        denom: denom.clone(),
218                        amount: amount.into(),
219                    }))
220            }
221            Self::Cw20 { .. } => Err(anyhow!(PerpError::new(
222                ErrorId::NativeFunds,
223                ErrorDomain::Wallet,
224                format!("{} cannot be turned into a native coin", self.name())
225            ))),
226        }
227    }
228
229    /// Round down to the supported precision of this token
230    pub fn round_down_to_precision(&self, amount: Collateral) -> Result<Collateral> {
231        self.from_u128(
232            self.into_u128(amount.into_decimal256())?
233                .unwrap_or_default(),
234        )
235        .map(Collateral::from_decimal256)
236    }
237
238    /// helper function
239    ///
240    /// when we know for a fact we have a WalletSource::Cw20
241    /// we can get a Send Execute messge from a Number amount
242    pub fn into_cw20_execute_send_msg<T: Serialize>(
243        &self,
244        contract: &Addr,
245        amount: Collateral,
246        submsg: &T,
247    ) -> Result<Option<Cw20ExecuteMsg>> {
248        match self {
249            Self::Native { .. } => Err(anyhow!(PerpError::new(
250                ErrorId::Cw20Funds,
251                ErrorDomain::Wallet,
252                format!("{} cannot be turned into a cw20 message", self.name())
253            ))),
254            Self::Cw20 { .. } => {
255                let msg = to_json_binary(submsg)?;
256                Ok(self
257                    .into_u128(amount.into_decimal256())?
258                    .map(|amount| Cw20ExecuteMsg::Send {
259                        contract: contract.into(),
260                        amount: amount.into(),
261                        msg,
262                    }))
263            }
264        }
265    }
266
267    /// helper function
268    ///
269    /// when we know for a fact we have a WalletSource::Cw20
270    /// we can get a Transfer Execute messge from a Number amount
271    pub fn into_cw20_execute_transfer_msg(
272        &self,
273        recipient: &Addr,
274        amount: NonZero<Collateral>,
275    ) -> Result<Option<Cw20ExecuteMsg>> {
276        match self {
277            Self::Native { .. } => Err(anyhow!(PerpError::new(
278                ErrorId::Cw20Funds,
279                ErrorDomain::Wallet,
280                format!("{} cannot be turned into a cw20 message", self.name())
281            ))),
282            Self::Cw20 { .. } => Ok(self.into_u128(amount.into_decimal256())?.map(|amount| {
283                Cw20ExecuteMsg::Transfer {
284                    recipient: recipient.into(),
285                    amount: amount.into(),
286                }
287            })),
288        }
289    }
290
291    /// perps-specific use-case for executing a market message with funds
292    pub fn into_market_execute_msg(
293        &self,
294        market_addr: &Addr,
295        amount: Collateral,
296        execute_msg: MarketExecuteMsg,
297    ) -> Result<WasmMsg> {
298        self.into_execute_msg(market_addr, amount, &execute_msg)
299    }
300
301    /// helper to create an execute message with funds
302    pub fn into_execute_msg<T: Serialize + std::fmt::Debug>(
303        &self,
304        contract_addr: &Addr,
305        amount: Collateral,
306        execute_msg: &T,
307    ) -> Result<WasmMsg> {
308        match self.clone() {
309            Self::Cw20 { addr, .. } => {
310                let msg = self
311                    .into_cw20_execute_send_msg(contract_addr, amount, &execute_msg)
312                    .map_err(|err| {
313                        let downcast = err
314                            .downcast_ref::<PerpError>()
315                            .map(|item| item.description.clone());
316                        let msg = format!("{downcast:?} (exec inner msg: {execute_msg:?})!");
317                        anyhow!(PerpError::new(
318                            ErrorId::Conversion,
319                            ErrorDomain::Wallet,
320                            msg
321                        ))
322                    })?;
323
324                match msg {
325                    Some(msg) => Ok(WasmMsg::Execute {
326                        contract_addr: addr.into_string(),
327                        msg: to_json_binary(&msg)?,
328                        funds: Vec::new(),
329                    }),
330                    None => {
331                        // no funds, so just send the execute_msg directly
332                        // to the contract
333                        Ok(WasmMsg::Execute {
334                            contract_addr: contract_addr.to_string(),
335                            msg: to_json_binary(&execute_msg)?,
336                            funds: Vec::new(),
337                        })
338                    }
339                }
340            }
341            Self::Native { .. } => {
342                let funds = if amount.is_zero() {
343                    Vec::new()
344                } else {
345                    let amount = NumberGtZero::new(amount.into_decimal256())
346                        .context("Unable to convert amount into NumberGtZero")?;
347                    let coin = self
348                        .into_native_coin(amount)
349                        .map_err(|err| {
350                            let downcast = err
351                                .downcast_ref::<PerpError>()
352                                .map(|item| item.description.clone());
353                            let msg = format!("{downcast:?} (exec inner msg: {execute_msg:?})!");
354                            anyhow!(PerpError::new(
355                                ErrorId::Conversion,
356                                ErrorDomain::Wallet,
357                                msg
358                            ))
359                        })?
360                        .unwrap();
361
362                    vec![coin]
363                };
364
365                let execute_msg = to_json_binary(&execute_msg)?;
366
367                Ok(WasmMsg::Execute {
368                    contract_addr: contract_addr.to_string(),
369                    msg: execute_msg,
370                    funds,
371                })
372            }
373        }
374    }
375
376    /// Validates that the given collateral doesn't require more precision
377    /// than what the token supports
378    pub fn validate_collateral(&self, value: NonZero<Collateral>) -> Result<NonZero<Collateral>> {
379        let value_decimal256 = value.into_decimal256();
380
381        if let Some(value_128) = self.into_u128(value_decimal256)? {
382            let value_truncated = self.from_u128(value_128)?;
383            if value_truncated == value_decimal256 {
384                return Ok(value);
385            }
386        }
387
388        let msg = format!("Token Collateral must be as precise as the Token (is {}, only {} decimal places supported)", value, self.decimal_places());
389        Err(anyhow!(PerpError::new(
390            ErrorId::Conversion,
391            ErrorDomain::Wallet,
392            msg
393        )))
394    }
395}