cw_utils/
payment.rs

1use cosmwasm_std::{Coin, MessageInfo, Uint128};
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<Uint128, 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<Uint128, PaymentError> {
44    if info.funds.is_empty() {
45        Ok(Uint128::zero())
46    } else if info.funds.len() == 1 && info.funds[0].denom == denom {
47        Ok(info.funds[0].amount)
48    } else {
49        // find first mis-match
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::mock_info;
77    use cosmwasm_std::{coin, coins};
78
79    const SENDER: &str = "sender";
80
81    #[test]
82    fn nonpayable_works() {
83        let no_payment = mock_info(SENDER, &[]);
84        nonpayable(&no_payment).unwrap();
85
86        let payment = mock_info(SENDER, &coins(100, "uatom"));
87        let res = nonpayable(&payment);
88        assert_eq!(res.unwrap_err(), PaymentError::NonPayable {});
89    }
90
91    #[test]
92    fn may_pay_works() {
93        let atom: &str = "uatom";
94        let no_payment = mock_info(SENDER, &[]);
95        let atom_payment = mock_info(SENDER, &coins(100, atom));
96        let eth_payment = mock_info(SENDER, &coins(100, "wei"));
97        let mixed_payment = mock_info(SENDER, &[coin(50, atom), coin(120, "wei")]);
98
99        let res = may_pay(&no_payment, atom).unwrap();
100        assert_eq!(res, Uint128::zero());
101
102        let res = may_pay(&atom_payment, atom).unwrap();
103        assert_eq!(res, Uint128::new(100));
104
105        let err = may_pay(&eth_payment, atom).unwrap_err();
106        assert_eq!(err, PaymentError::ExtraDenom("wei".to_string()));
107
108        let err = may_pay(&mixed_payment, atom).unwrap_err();
109        assert_eq!(err, PaymentError::ExtraDenom("wei".to_string()));
110    }
111
112    #[test]
113    fn must_pay_works() {
114        let atom: &str = "uatom";
115        let no_payment = mock_info(SENDER, &[]);
116        let atom_payment = mock_info(SENDER, &coins(100, atom));
117        let zero_payment = mock_info(SENDER, &coins(0, atom));
118        let eth_payment = mock_info(SENDER, &coins(100, "wei"));
119        let mixed_payment = mock_info(SENDER, &[coin(50, atom), coin(120, "wei")]);
120
121        let res = must_pay(&atom_payment, atom).unwrap();
122        assert_eq!(res, Uint128::new(100));
123
124        let err = must_pay(&no_payment, atom).unwrap_err();
125        assert_eq!(err, PaymentError::NoFunds {});
126
127        let err = must_pay(&zero_payment, atom).unwrap_err();
128        assert_eq!(err, PaymentError::NoFunds {});
129
130        let err = must_pay(&eth_payment, atom).unwrap_err();
131        assert_eq!(err, PaymentError::MissingDenom(atom.to_string()));
132
133        let err = must_pay(&mixed_payment, atom).unwrap_err();
134        assert_eq!(err, PaymentError::MultipleDenoms {});
135    }
136}