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 write!(f, "{}{}", self.amount, self.denom)
57 }
58}
59
60pub fn coins(amount: u128, denom: impl Into<String>) -> Vec<Coin> {
78 vec![coin(amount, denom)]
79}
80
81pub 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: 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 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}