cosmwasm_std/
coin.rs

1use core::{fmt, str::FromStr};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5use crate::prelude::*;
6use crate::CoinFromStrError;
7use crate::Uint256;
8
9#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema)]
10pub struct Coin {
11    pub denom: String,
12    pub amount: Uint256,
13}
14
15impl Coin {
16    pub fn new(amount: impl Into<Uint256>, denom: impl Into<String>) -> Self {
17        Coin {
18            amount: amount.into(),
19            denom: denom.into(),
20        }
21    }
22}
23
24impl fmt::Debug for Coin {
25    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
26        write!(f, "Coin {{ {} \"{}\" }}", self.amount, self.denom)
27    }
28}
29
30impl FromStr for Coin {
31    type Err = CoinFromStrError;
32
33    fn from_str(s: &str) -> Result<Self, Self::Err> {
34        let pos = s
35            .find(|c: char| !c.is_ascii_digit())
36            .ok_or(CoinFromStrError::MissingDenom)?;
37        let (amount, denom) = s.split_at(pos);
38
39        if amount.is_empty() {
40            return Err(CoinFromStrError::MissingAmount);
41        }
42
43        Ok(Coin {
44            amount: amount.parse::<u128>()?.into(),
45            denom: denom.to_string(),
46        })
47    }
48}
49
50impl fmt::Display for Coin {
51    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
52        // We use the formatting without a space between amount and denom,
53        // which is common in the Cosmos SDK ecosystem:
54        // https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/types/coin.go#L643-L645
55        // For communication to end users, Coin needs to transformed anyway (e.g. convert integer uatom to decimal ATOM).
56        write!(f, "{}{}", self.amount, self.denom)
57    }
58}
59
60/// A shortcut constructor for a set of one denomination of coins
61///
62/// # Examples
63///
64/// ```
65/// # use cosmwasm_std::{coins, BankMsg, CosmosMsg, Response, SubMsg};
66/// # use cosmwasm_std::testing::mock_env;
67/// # let env = mock_env();
68/// # let recipient = "blub".to_string();
69/// let tip = coins(123, "ucosm");
70///
71/// let mut response: Response = Default::default();
72/// response.messages = vec![SubMsg::new(BankMsg::Send {
73///   to_address: recipient,
74///   amount: tip,
75/// })];
76/// ```
77pub fn coins(amount: u128, denom: impl Into<String>) -> Vec<Coin> {
78    vec![coin(amount, denom)]
79}
80
81/// A shorthand constructor for Coin
82///
83/// # Examples
84///
85/// ```
86/// # use cosmwasm_std::{coin, BankMsg, CosmosMsg, Response, SubMsg};
87/// # let recipient = "blub".to_string();
88/// let tip = vec![
89///     coin(123, "ucosm"),
90///     coin(24, "ustake"),
91/// ];
92///
93/// let mut response: Response = Default::default();
94/// response.messages = vec![SubMsg::new(BankMsg::Send {
95///     to_address: recipient,
96///     amount: tip,
97/// })];
98/// ```
99pub fn coin(amount: u128, denom: impl Into<String>) -> Coin {
100    Coin::new(amount, denom)
101}
102
103/// has_coins returns true if the list of coins has at least the required amount
104pub fn has_coins(coins: &[Coin], required: &Coin) -> bool {
105    coins
106        .iter()
107        .find(|c| c.denom == required.denom)
108        .map(|m| m.amount >= required.amount)
109        .unwrap_or(false)
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn coin_implements_display() {
118        let a = Coin {
119            amount: Uint256::new(123),
120            denom: "ucosm".to_string(),
121        };
122
123        let embedded = format!("Amount: {a}");
124        assert_eq!(embedded, "Amount: 123ucosm");
125        assert_eq!(a.to_string(), "123ucosm");
126    }
127
128    #[test]
129    fn coin_works() {
130        let a = coin(123, "ucosm");
131        assert_eq!(
132            a,
133            Coin {
134                amount: Uint256::new(123),
135                denom: "ucosm".to_string()
136            }
137        );
138
139        let zero = coin(0, "ucosm");
140        assert_eq!(
141            zero,
142            Coin {
143                amount: Uint256::new(0),
144                denom: "ucosm".to_string()
145            }
146        );
147
148        let string_denom = coin(42, String::from("ucosm"));
149        assert_eq!(
150            string_denom,
151            Coin {
152                amount: Uint256::new(42),
153                denom: "ucosm".to_string()
154            }
155        );
156    }
157
158    #[test]
159    fn coins_works() {
160        let a = coins(123, "ucosm");
161        assert_eq!(
162            a,
163            vec![Coin {
164                amount: Uint256::new(123),
165                denom: "ucosm".to_string()
166            }]
167        );
168
169        let zero = coins(0, "ucosm");
170        assert_eq!(
171            zero,
172            vec![Coin {
173                amount: Uint256::new(0),
174                denom: "ucosm".to_string()
175            }]
176        );
177
178        let string_denom = coins(42, String::from("ucosm"));
179        assert_eq!(
180            string_denom,
181            vec![Coin {
182                amount: Uint256::new(42),
183                denom: "ucosm".to_string()
184            }]
185        );
186    }
187
188    #[test]
189    fn has_coins_matches() {
190        let wallet = vec![coin(12345, "ETH"), coin(555, "BTC")];
191
192        // less than same type
193        assert!(has_coins(&wallet, &coin(777, "ETH")));
194    }
195
196    #[test]
197    fn parse_coin() {
198        let expected = Coin::new(123u128, "ucosm");
199        assert_eq!("123ucosm".parse::<Coin>().unwrap(), expected);
200        // leading zeroes should be ignored
201        assert_eq!("00123ucosm".parse::<Coin>().unwrap(), expected);
202        // 0 amount parses correctly
203        assert_eq!("0ucosm".parse::<Coin>().unwrap(), Coin::new(0u128, "ucosm"));
204        // ibc denom should work
205        let ibc_str = "11111ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2";
206        let ibc_coin = Coin::new(
207            11111u128,
208            "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2",
209        );
210        assert_eq!(ibc_str.parse::<Coin>().unwrap(), ibc_coin);
211
212        // error cases
213        assert_eq!(
214            Coin::from_str("123").unwrap_err(),
215            CoinFromStrError::MissingDenom
216        );
217        assert_eq!(
218            Coin::from_str("ucosm").unwrap_err(), // no amount
219            CoinFromStrError::MissingAmount
220        );
221        assert_eq!(
222            Coin::from_str("-123ucosm").unwrap_err(), // negative amount
223            CoinFromStrError::MissingAmount
224        );
225        assert_eq!(
226            Coin::from_str("").unwrap_err(), // empty input
227            CoinFromStrError::MissingDenom
228        );
229        assert_eq!(
230            Coin::from_str(" 1ucosm").unwrap_err(), // unsupported whitespace
231            CoinFromStrError::MissingAmount
232        );
233        assert_eq!(
234            Coin::from_str("�1ucosm").unwrap_err(), // other broken data
235            CoinFromStrError::MissingAmount
236        );
237        assert_eq!(
238            Coin::from_str("340282366920938463463374607431768211456ucosm")
239                .unwrap_err()
240                .to_string(),
241            "Invalid amount: number too large to fit in target type"
242        );
243    }
244
245    #[test]
246    fn debug_coin() {
247        let coin = Coin::new(123u128, "ucosm");
248        assert_eq!(format!("{coin:?}"), r#"Coin { 123 "ucosm" }"#);
249    }
250}