cw_simple_assets/
lib.rs

1use cosmwasm_schema::cw_serde;
2use cosmwasm_std::{
3    coin, coins, wasm_execute, Addr, Api, BankMsg, Coin, CosmosMsg, MessageInfo, StdError,
4    StdResult, Uint128, WasmMsg,
5};
6
7use thiserror::Error;
8
9#[cw_serde]
10pub enum Token {
11    Native { denom: String },
12    Cw20 { address: Addr },
13}
14
15impl Token {
16    pub fn new_native(denom: &str) -> Self {
17        Self::Native {
18            denom: denom.to_string(),
19        }
20    }
21
22    pub fn new_cw20(address: &Addr) -> Self {
23        Self::Cw20 {
24            address: address.to_owned(),
25        }
26    }
27
28    pub fn is_native(&self) -> bool {
29        match self {
30            Self::Native { denom: _ } => true,
31            Self::Cw20 { address: _ } => false,
32        }
33    }
34
35    pub fn try_get_native(&self) -> StdResult<String> {
36        match self {
37            Self::Native { denom } => Ok(denom.to_string()),
38            Self::Cw20 { address: _ } => Err(AssetError::AssetIsNotFound)?,
39        }
40    }
41
42    pub fn try_get_cw20(&self) -> StdResult<Addr> {
43        match self {
44            Self::Native { denom: _ } => Err(AssetError::AssetIsNotFound)?,
45            Self::Cw20 { address } => Ok(address.to_owned()),
46        }
47    }
48
49    pub fn get_symbol(&self) -> String {
50        match self {
51            Self::Native { denom } => denom.to_string(),
52            Self::Cw20 { address } => address.to_string(),
53        }
54    }
55}
56
57impl From<String> for Token {
58    fn from(denom: String) -> Self {
59        Self::Native { denom }
60    }
61}
62
63impl From<Addr> for Token {
64    fn from(address: Addr) -> Self {
65        Self::Cw20 { address }
66    }
67}
68
69#[cw_serde]
70pub enum TokenUnverified {
71    Native { denom: String },
72    Cw20 { address: String },
73}
74
75impl TokenUnverified {
76    pub fn new_native(denom: &str) -> Self {
77        Self::Native {
78            denom: denom.to_string(),
79        }
80    }
81
82    pub fn new_cw20(address: &str) -> Self {
83        Self::Cw20 {
84            address: address.to_string(),
85        }
86    }
87
88    pub fn verify(&self, api: &dyn Api) -> StdResult<Token> {
89        match self {
90            Self::Cw20 { address } => Ok(Token::new_cw20(&api.addr_validate(address)?)),
91            Self::Native { denom } => Ok(Token::new_native(denom)),
92        }
93    }
94
95    pub fn get_symbol(&self) -> String {
96        match self.to_owned() {
97            Self::Native { denom } => denom,
98            Self::Cw20 { address } => address,
99        }
100    }
101}
102
103impl From<Token> for TokenUnverified {
104    fn from(token: Token) -> Self {
105        match token {
106            Token::Native { denom } => Self::Native { denom },
107            Token::Cw20 { address } => Self::Cw20 {
108                address: address.to_string(),
109            },
110        }
111    }
112}
113
114#[cw_serde]
115pub struct Currency<T: From<Token>> {
116    pub token: T,
117    pub decimals: u8,
118}
119
120impl Default for Currency<Token> {
121    fn default() -> Self {
122        Self::new(&Token::new_native(&String::default()), 0)
123    }
124}
125
126impl<T: From<Token> + Clone> Currency<T> {
127    pub fn new(denom_or_address: &T, decimals: u8) -> Self {
128        Self {
129            token: denom_or_address.to_owned(),
130            decimals,
131        }
132    }
133}
134
135#[cw_serde]
136pub struct InfoResp {
137    pub sender: Addr,
138    pub asset_amount: Uint128,
139    pub asset_token: Token,
140}
141
142#[cw_serde]
143pub enum Funds {
144    Empty,
145    Single {
146        sender: Option<String>,
147        amount: Option<Uint128>,
148    },
149}
150
151impl Funds {
152    pub fn empty() -> Self {
153        Self::Empty
154    }
155
156    pub fn single(sender: Option<String>, amount: Option<Uint128>) -> Self {
157        Self::Single { sender, amount }
158    }
159
160    /// Supports both native and cw20 tokens                                        \
161    /// * Funds::empty() to check if info.funds is empty                            \
162    /// * Funds::single(None, None) to check native token                           \
163    /// * Funds::single(Some(msg.sender), Some(msg.amount)) to check cw20 token
164    pub fn check(&self, api: &dyn Api, info: &MessageInfo) -> StdResult<InfoResp> {
165        match self {
166            Funds::Empty => {
167                nonpayable(info)?;
168
169                Ok(InfoResp {
170                    sender: info.sender.to_owned(),
171                    asset_amount: Uint128::zero(),
172                    asset_token: Token::new_native(&String::default()),
173                })
174            }
175            Funds::Single { sender, amount } => {
176                if sender.as_ref().is_none() || amount.is_none() {
177                    let Coin { denom, amount } = one_coin(info)?;
178
179                    Ok(InfoResp {
180                        sender: info.sender.to_owned(),
181                        asset_amount: amount,
182                        asset_token: Token::new_native(&denom),
183                    })
184                } else {
185                    Ok(InfoResp {
186                        sender: api.addr_validate(
187                            sender.as_ref().ok_or(AssetError::WrongFundsCombination)?,
188                        )?,
189                        asset_amount: amount.ok_or(AssetError::WrongFundsCombination)?,
190                        asset_token: Token::new_cw20(&info.sender),
191                    })
192                }
193            }
194        }
195    }
196}
197
198pub fn add_funds_to_exec_msg(
199    exec_msg: &WasmMsg,
200    funds_list: &[(Uint128, Token)],
201) -> StdResult<WasmMsg> {
202    let mut native_tokens: Vec<Coin> = vec![];
203    let mut cw20_tokens: Vec<(Uint128, Addr)> = vec![];
204
205    for (amount, token) in funds_list {
206        match token {
207            Token::Native { denom } => {
208                native_tokens.push(coin(amount.u128(), denom));
209            }
210            Token::Cw20 { address } => {
211                cw20_tokens.push((*amount, address.to_owned()));
212            }
213        }
214    }
215
216    match exec_msg {
217        WasmMsg::Execute {
218            contract_addr, msg, ..
219        } => {
220            // Case 1 `Deposit` - only native tokens
221            if cw20_tokens.is_empty() {
222                return Ok(WasmMsg::Execute {
223                    contract_addr: contract_addr.to_string(),
224                    msg: msg.to_owned(),
225                    funds: native_tokens,
226                });
227            }
228
229            // Case 2 `Swap` - only single cw20 token
230            if (cw20_tokens.len() == 1) && native_tokens.is_empty() {
231                let (amount, token_address) =
232                    cw20_tokens.first().ok_or(AssetError::AssetIsNotFound)?;
233
234                return wasm_execute(
235                    token_address,
236                    &cw20::Cw20ExecuteMsg::Send {
237                        contract: contract_addr.to_string(),
238                        amount: amount.to_owned(),
239                        msg: msg.to_owned(),
240                    },
241                    vec![],
242                );
243            }
244
245            Err(AssetError::WrongFundsCombination)?
246        }
247        _ => Err(AssetError::WrongActionType)?,
248    }
249}
250
251pub fn get_transfer_msg(recipient: &Addr, amount: Uint128, token: &Token) -> StdResult<CosmosMsg> {
252    Ok(match token {
253        Token::Native { denom } => CosmosMsg::Bank(BankMsg::Send {
254            to_address: recipient.to_string(),
255            amount: coins(amount.u128(), denom),
256        }),
257        Token::Cw20 { address } => CosmosMsg::Wasm(wasm_execute(
258            address,
259            &cw20::Cw20ExecuteMsg::Transfer {
260                recipient: recipient.to_string(),
261                amount,
262            },
263            vec![],
264        )?),
265    })
266}
267
268/// If exactly one coin was sent, returns it regardless of denom.
269/// Returns error if 0 or 2+ coins were sent
270fn one_coin(info: &MessageInfo) -> StdResult<Coin> {
271    if info.funds.len() != 1 {
272        Err(AssetError::NonSingleDenom)?;
273    }
274
275    if let Some(coin) = info.funds.first() {
276        if !coin.amount.is_zero() {
277            return Ok(coin.to_owned());
278        }
279    }
280
281    Err(AssetError::ZeroCoins)?
282}
283
284/// returns an error if any coins were sent
285fn nonpayable(info: &MessageInfo) -> StdResult<()> {
286    if !info.funds.is_empty() {
287        Err(AssetError::ShouldNotAcceptFunds)?;
288    }
289
290    Ok(())
291}
292
293#[derive(Error, Debug, PartialEq)]
294pub enum AssetError {
295    #[error("Asset isn't found!")]
296    AssetIsNotFound,
297
298    #[error("Wrong funds combination!")]
299    WrongFundsCombination,
300
301    #[error("Wrong action type!")]
302    WrongActionType,
303
304    #[error("Coins amount is zero!")]
305    ZeroCoins,
306
307    #[error("Amount of denoms isn't equal 1!")]
308    NonSingleDenom,
309
310    #[error("This message doesn't accept funds!")]
311    ShouldNotAcceptFunds,
312}
313
314impl From<AssetError> for StdError {
315    fn from(asset_error: AssetError) -> Self {
316        Self::generic_err(asset_error.to_string())
317    }
318}
319
320#[cfg(test)]
321pub mod test {
322    use super::*;
323    use cosmwasm_std::testing::{message_info, mock_dependencies};
324
325    #[test]
326    fn test_single_coin() -> StdResult<()> {
327        const ADMIN: &str = "cosmwasm105yqjjdgl00nzwyj9aua98zgetdn4qyhukjf5t";
328        const AMOUNT: u128 = 100;
329        const DENOM: &str = "cosm";
330
331        let deps = mock_dependencies();
332        let info = message_info(&Addr::unchecked(ADMIN), &coins(AMOUNT, DENOM));
333        let info_resp = Funds::single(None, None).check(&deps.api, &info)?;
334
335        assert_eq!(
336            info_resp,
337            InfoResp {
338                sender: Addr::unchecked(ADMIN),
339                asset_amount: Uint128::new(AMOUNT),
340                asset_token: Token::new_native(DENOM),
341            }
342        );
343
344        Ok(())
345    }
346}