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::Uint128;
8
9#[derive(Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema)]
10pub struct Coin {
11    pub denom: String,
12    pub amount: Uint128,
13}
14
15impl Coin {
16    pub fn new(amount: impl Into<Uint128>, 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 anways (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/// # use cosmwasm_std::testing::{mock_env, mock_info};
88/// # let env = mock_env();
89/// # let recipient = "blub".to_string();
90/// let tip = vec![
91///     coin(123, "ucosm"),
92///     coin(24, "ustake"),
93/// ];
94///
95/// let mut response: Response = Default::default();
96/// response.messages = vec![SubMsg::new(BankMsg::Send {
97///     to_address: recipient,
98///     amount: tip,
99/// })];
100/// ```
101pub fn coin(amount: u128, denom: impl Into<String>) -> Coin {
102    Coin::new(amount, denom)
103}
104
105/// has_coins returns true if the list of coins has at least the required amount
106pub fn has_coins(coins: &[Coin], required: &Coin) -> bool {
107    coins
108        .iter()
109        .find(|c| c.denom == required.denom)
110        .map(|m| m.amount >= required.amount)
111        .unwrap_or(false)
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn coin_implements_display() {
120        let a = Coin {
121            amount: Uint128::new(123),
122            denom: "ucosm".to_string(),
123        };
124
125        let embedded = format!("Amount: {a}");
126        assert_eq!(embedded, "Amount: 123ucosm");
127        assert_eq!(a.to_string(), "123ucosm");
128    }
129
130    #[test]
131    fn coin_works() {
132        let a = coin(123, "ucosm");
133        assert_eq!(
134            a,
135            Coin {
136                amount: Uint128::new(123),
137                denom: "ucosm".to_string()
138            }
139        );
140
141        let zero = coin(0, "ucosm");
142        assert_eq!(
143            zero,
144            Coin {
145                amount: Uint128::new(0),
146                denom: "ucosm".to_string()
147            }
148        );
149
150        let string_denom = coin(42, String::from("ucosm"));
151        assert_eq!(
152            string_denom,
153            Coin {
154                amount: Uint128::new(42),
155                denom: "ucosm".to_string()
156            }
157        );
158    }
159
160    #[test]
161    fn coins_works() {
162        let a = coins(123, "ucosm");
163        assert_eq!(
164            a,
165            vec![Coin {
166                amount: Uint128::new(123),
167                denom: "ucosm".to_string()
168            }]
169        );
170
171        let zero = coins(0, "ucosm");
172        assert_eq!(
173            zero,
174            vec![Coin {
175                amount: Uint128::new(0),
176                denom: "ucosm".to_string()
177            }]
178        );
179
180        let string_denom = coins(42, String::from("ucosm"));
181        assert_eq!(
182            string_denom,
183            vec![Coin {
184                amount: Uint128::new(42),
185                denom: "ucosm".to_string()
186            }]
187        );
188    }
189
190    #[test]
191    fn has_coins_matches() {
192        let wallet = vec![coin(12345, "ETH"), coin(555, "BTC")];
193
194        // less than same type
195        assert!(has_coins(&wallet, &coin(777, "ETH")));
196    }
197
198    #[test]
199    fn parse_coin() {
200        let expected = Coin::new(123u128, "ucosm");
201        assert_eq!("123ucosm".parse::<Coin>().unwrap(), expected);
202        // leading zeroes should be ignored
203        assert_eq!("00123ucosm".parse::<Coin>().unwrap(), expected);
204        // 0 amount parses correctly
205        assert_eq!("0ucosm".parse::<Coin>().unwrap(), Coin::new(0u128, "ucosm"));
206        // ibc denom should work
207        let ibc_str = "11111ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2";
208        let ibc_coin = Coin::new(
209            11111u128,
210            "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2",
211        );
212        assert_eq!(ibc_str.parse::<Coin>().unwrap(), ibc_coin);
213
214        // error cases
215        assert_eq!(
216            Coin::from_str("123").unwrap_err(),
217            CoinFromStrError::MissingDenom
218        );
219        assert_eq!(
220            Coin::from_str("ucosm").unwrap_err(), // no amount
221            CoinFromStrError::MissingAmount
222        );
223        assert_eq!(
224            Coin::from_str("-123ucosm").unwrap_err(), // negative amount
225            CoinFromStrError::MissingAmount
226        );
227        assert_eq!(
228            Coin::from_str("").unwrap_err(), // empty input
229            CoinFromStrError::MissingDenom
230        );
231        assert_eq!(
232            Coin::from_str(" 1ucosm").unwrap_err(), // unsupported whitespace
233            CoinFromStrError::MissingAmount
234        );
235        assert_eq!(
236            Coin::from_str("�1ucosm").unwrap_err(), // other broken data
237            CoinFromStrError::MissingAmount
238        );
239        assert_eq!(
240            Coin::from_str("340282366920938463463374607431768211456ucosm")
241                .unwrap_err()
242                .to_string(),
243            "Invalid amount: number too large to fit in target type"
244        );
245    }
246
247    #[test]
248    fn debug_coin() {
249        let coin = Coin::new(123u128, "ucosm");
250        assert_eq!(format!("{coin:?}"), r#"Coin { 123 "ucosm" }"#);
251    }
252}