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(
10    Serialize, Deserialize, Clone, Default, PartialEq, Eq, JsonSchema, cw_schema::Schemaifier,
11)]
12pub struct Coin {
13    pub denom: String,
14    pub amount: Uint256,
15}
16
17impl Coin {
18    pub fn new(amount: impl Into<Uint256>, denom: impl Into<String>) -> Self {
19        Coin {
20            amount: amount.into(),
21            denom: denom.into(),
22        }
23    }
24}
25
26impl fmt::Debug for Coin {
27    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
28        write!(f, "Coin {{ {} \"{}\" }}", self.amount, self.denom)
29    }
30}
31
32impl FromStr for Coin {
33    type Err = CoinFromStrError;
34
35    fn from_str(s: &str) -> Result<Self, Self::Err> {
36        let pos = s
37            .find(|c: char| !c.is_ascii_digit())
38            .ok_or(CoinFromStrError::MissingDenom)?;
39        let (amount, denom) = s.split_at(pos);
40
41        if amount.is_empty() {
42            return Err(CoinFromStrError::MissingAmount);
43        }
44
45        Ok(Coin {
46            amount: amount.parse::<u128>()?.into(),
47            denom: denom.to_string(),
48        })
49    }
50}
51
52impl fmt::Display for Coin {
53    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54        // We use the formatting without a space between amount and denom,
55        // which is common in the Cosmos SDK ecosystem:
56        // https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/types/coin.go#L643-L645
57        // For communication to end users, Coin needs to transformed anyway (e.g. convert integer uatom to decimal ATOM).
58        write!(f, "{}{}", self.amount, self.denom)
59    }
60}
61
62/// A shortcut constructor for a set of one denomination of coins
63///
64/// # Examples
65///
66/// ```
67/// # use cosmwasm_std::{coins, BankMsg, CosmosMsg, Response, SubMsg};
68/// # use cosmwasm_std::testing::mock_env;
69/// # let env = mock_env();
70/// # let recipient = "blub".to_string();
71/// let tip = coins(123, "ucosm");
72///
73/// let mut response: Response = Default::default();
74/// response.messages = vec![SubMsg::new(BankMsg::Send {
75///   to_address: recipient,
76///   amount: tip,
77/// })];
78/// ```
79pub fn coins(amount: u128, denom: impl Into<String>) -> Vec<Coin> {
80    vec![coin(amount, denom)]
81}
82
83/// A shorthand constructor for Coin
84///
85/// # Examples
86///
87/// ```
88/// # use cosmwasm_std::{coin, BankMsg, CosmosMsg, Response, SubMsg};
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: Uint256::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: Uint256::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: Uint256::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: Uint256::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: Uint256::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: Uint256::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: Uint256::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}