cw_utils/
payment.rs

1use cosmwasm_std::{Coin, MessageInfo, Uint256};
2use thiserror::Error;
3
4/// returns an error if any coins were sent
5pub fn nonpayable(info: &MessageInfo) -> Result<(), PaymentError> {
6    if info.funds.is_empty() {
7        Ok(())
8    } else {
9        Err(PaymentError::NonPayable {})
10    }
11}
12
13/// If exactly one coin was sent, returns it regardless of denom.
14/// Returns error if 0 or 2+ coins were sent
15pub fn one_coin(info: &MessageInfo) -> Result<Coin, PaymentError> {
16    match info.funds.len() {
17        0 => Err(PaymentError::NoFunds {}),
18        1 => {
19            let coin = &info.funds[0];
20            if coin.amount.is_zero() {
21                Err(PaymentError::NoFunds {})
22            } else {
23                Ok(coin.clone())
24            }
25        }
26        _ => Err(PaymentError::MultipleDenoms {}),
27    }
28}
29
30/// Requires exactly one denom sent, which matches the requested denom.
31/// Returns the amount if only one denom and non-zero amount. Errors otherwise.
32pub fn must_pay(info: &MessageInfo, denom: &str) -> Result<Uint256, PaymentError> {
33    let coin = one_coin(info)?;
34    if coin.denom != denom {
35        Err(PaymentError::MissingDenom(denom.to_string()))
36    } else {
37        Ok(coin.amount)
38    }
39}
40
41/// Similar to must_pay, but it any payment is optional. Returns an error if a different
42/// denom was sent. Otherwise, returns the amount of `denom` sent, or 0 if nothing sent.
43pub fn may_pay(info: &MessageInfo, denom: &str) -> Result<Uint256, PaymentError> {
44    if info.funds.is_empty() {
45        Ok(Uint256::zero())
46    } else if info.funds.len() == 1 && info.funds[0].denom == denom {
47        Ok(info.funds[0].amount)
48    } else {
49        // find first mismatch
50        let wrong = info.funds.iter().find(|c| c.denom != denom).unwrap();
51        Err(PaymentError::ExtraDenom(wrong.denom.to_string()))
52    }
53}
54
55#[derive(Error, Debug, PartialEq, Eq)]
56pub enum PaymentError {
57    #[error("Must send reserve token '{0}'")]
58    MissingDenom(String),
59
60    #[error("Received unsupported denom '{0}'")]
61    ExtraDenom(String),
62
63    #[error("Sent more than one denomination")]
64    MultipleDenoms {},
65
66    #[error("No funds sent")]
67    NoFunds {},
68
69    #[error("This message does no accept funds")]
70    NonPayable {},
71}
72
73#[cfg(test)]
74mod test {
75    use super::*;
76    use cosmwasm_std::testing::{message_info, mock_dependencies};
77    use cosmwasm_std::{coin, coins};
78
79    const SENDER: &str = "sender";
80
81    #[test]
82    fn nonpayable_works() {
83        let deps = mock_dependencies();
84        let sender = deps.api.addr_make(SENDER);
85
86        let no_payment = message_info(&sender, &[]);
87        nonpayable(&no_payment).unwrap();
88
89        let payment = message_info(&sender, &coins(100, "uatom"));
90        let res = nonpayable(&payment);
91        assert_eq!(res.unwrap_err(), PaymentError::NonPayable {});
92    }
93
94    #[test]
95    fn may_pay_works() {
96        let deps = mock_dependencies();
97        let sender = deps.api.addr_make(SENDER);
98
99        let atom: &str = "uatom";
100        let no_payment = message_info(&sender, &[]);
101        let atom_payment = message_info(&sender, &coins(100, atom));
102        let eth_payment = message_info(&sender, &coins(100, "wei"));
103        let mixed_payment = message_info(&sender, &[coin(50, atom), coin(120, "wei")]);
104
105        let res = may_pay(&no_payment, atom).unwrap();
106        assert_eq!(res, Uint256::zero());
107
108        let res = may_pay(&atom_payment, atom).unwrap();
109        assert_eq!(res, Uint256::new(100));
110
111        let err = may_pay(&eth_payment, atom).unwrap_err();
112        assert_eq!(err, PaymentError::ExtraDenom("wei".to_string()));
113
114        let err = may_pay(&mixed_payment, atom).unwrap_err();
115        assert_eq!(err, PaymentError::ExtraDenom("wei".to_string()));
116    }
117
118    #[test]
119    fn must_pay_works() {
120        let deps = mock_dependencies();
121        let sender = deps.api.addr_make(SENDER);
122
123        let atom: &str = "uatom";
124        let no_payment = message_info(&sender, &[]);
125        let atom_payment = message_info(&sender, &coins(100, atom));
126        let zero_payment = message_info(&sender, &coins(0, atom));
127        let eth_payment = message_info(&sender, &coins(100, "wei"));
128        let mixed_payment = message_info(&sender, &[coin(50, atom), coin(120, "wei")]);
129
130        let res = must_pay(&atom_payment, atom).unwrap();
131        assert_eq!(res, Uint256::new(100));
132
133        let err = must_pay(&no_payment, atom).unwrap_err();
134        assert_eq!(err, PaymentError::NoFunds {});
135
136        let err = must_pay(&zero_payment, atom).unwrap_err();
137        assert_eq!(err, PaymentError::NoFunds {});
138
139        let err = must_pay(&eth_payment, atom).unwrap_err();
140        assert_eq!(err, PaymentError::MissingDenom(atom.to_string()));
141
142        let err = must_pay(&mixed_payment, atom).unwrap_err();
143        assert_eq!(err, PaymentError::MultipleDenoms {});
144    }
145}