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 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 {
100 Coin::new(amount, denom)
101}
102
103pub 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 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 assert_eq!("00123ucosm".parse::<Coin>().unwrap(), expected);
202 assert_eq!("0ucosm".parse::<Coin>().unwrap(), Coin::new(0u128, "ucosm"));
204 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 assert_eq!(
214 Coin::from_str("123").unwrap_err(),
215 CoinFromStrError::MissingDenom
216 );
217 assert_eq!(
218 Coin::from_str("ucosm").unwrap_err(), CoinFromStrError::MissingAmount
220 );
221 assert_eq!(
222 Coin::from_str("-123ucosm").unwrap_err(), CoinFromStrError::MissingAmount
224 );
225 assert_eq!(
226 Coin::from_str("").unwrap_err(), CoinFromStrError::MissingDenom
228 );
229 assert_eq!(
230 Coin::from_str(" 1ucosm").unwrap_err(), CoinFromStrError::MissingAmount
232 );
233 assert_eq!(
234 Coin::from_str("�1ucosm").unwrap_err(), 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}