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 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 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 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
268fn 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
284fn 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}