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 write!(f, "{}{}", self.amount, self.denom)
59 }
60}
61
62pub fn coins(amount: u128, denom: impl Into<String>) -> Vec<Coin> {
80 vec![coin(amount, denom)]
81}
82
83pub fn coin(amount: u128, denom: impl Into<String>) -> Coin {
102 Coin::new(amount, denom)
103}
104
105pub 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 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 assert_eq!("00123ucosm".parse::<Coin>().unwrap(), expected);
204 assert_eq!("0ucosm".parse::<Coin>().unwrap(), Coin::new(0u128, "ucosm"));
206 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 assert_eq!(
216 Coin::from_str("123").unwrap_err(),
217 CoinFromStrError::MissingDenom
218 );
219 assert_eq!(
220 Coin::from_str("ucosm").unwrap_err(), CoinFromStrError::MissingAmount
222 );
223 assert_eq!(
224 Coin::from_str("-123ucosm").unwrap_err(), CoinFromStrError::MissingAmount
226 );
227 assert_eq!(
228 Coin::from_str("").unwrap_err(), CoinFromStrError::MissingDenom
230 );
231 assert_eq!(
232 Coin::from_str(" 1ucosm").unwrap_err(), CoinFromStrError::MissingAmount
234 );
235 assert_eq!(
236 Coin::from_str("�1ucosm").unwrap_err(), 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}