ibc_relayer_types/applications/transfer/
coin.rs1use std::fmt::{Display, Error as FmtError, Formatter};
2use std::str::FromStr;
3
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6
7use ibc_proto::cosmos::base::v1beta1::Coin as ProtoCoin;
8
9use crate::serializers::serde_string;
10
11use super::amount::Amount;
12use super::denom::{BaseDenom, PrefixedDenom};
13use super::error::Error;
14
15pub type PrefixedCoin = Coin<PrefixedDenom>;
17
18pub type BaseCoin = Coin<BaseDenom>;
20
21pub type RawCoin = Coin<String>;
22
23#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
25pub struct Coin<D> {
26 pub denom: D,
28 #[serde(with = "serde_string")]
30 pub amount: Amount,
31}
32
33impl<D> Coin<D> {
34 pub fn new(denom: D, amount: impl Into<Amount>) -> Self {
35 Self {
36 denom,
37 amount: amount.into(),
38 }
39 }
40
41 pub fn checked_add(self, rhs: impl Into<Amount>) -> Option<Self> {
42 let amount = self.amount.checked_add(rhs)?;
43 Some(Self::new(self.denom, amount))
44 }
45
46 pub fn checked_sub(self, rhs: impl Into<Amount>) -> Option<Self> {
47 let amount = self.amount.checked_sub(rhs)?;
48 Some(Self::new(self.denom, amount))
49 }
50}
51
52impl<D: FromStr> Coin<D>
53where
54 D::Err: Into<Error>,
55{
56 pub fn from_string_list(coin_str: &str) -> Result<Vec<Self>, Error> {
57 coin_str.split(',').map(FromStr::from_str).collect()
58 }
59}
60
61impl<D: FromStr> FromStr for Coin<D>
62where
63 D::Err: Into<Error>,
64{
65 type Err = Error;
66
67 fn from_str(coin_str: &str) -> Result<Self, Error> {
69 let regex = Regex::new(
74 r"^(?<amount>[0-9]+(?:\.[0-9]+)?|\.[0-9]+)\s*(?<denom>[a-zA-Z][a-zA-Z0-9/:._-]{2,127})$",
75 )
76 .expect("failed to compile regex");
77
78 let captures = regex.captures(coin_str).ok_or_else(|| {
79 Error::invalid_coin(format!("{coin_str} (expected format: <amount><denom>)"))
80 })?;
81
82 let amount = captures["amount"].parse()?;
83 let denom = captures["denom"].parse().map_err(Into::into)?;
84
85 Ok(Coin { amount, denom })
86 }
87}
88
89impl<D: FromStr> TryFrom<ProtoCoin> for Coin<D>
90where
91 D::Err: Into<Error>,
92{
93 type Error = Error;
94
95 fn try_from(proto: ProtoCoin) -> Result<Coin<D>, Self::Error> {
96 let denom = D::from_str(&proto.denom).map_err(Into::into)?;
97 let amount = Amount::from_str(&proto.amount)?;
98 Ok(Self { denom, amount })
99 }
100}
101
102impl<D: ToString> From<Coin<D>> for ProtoCoin {
103 fn from(coin: Coin<D>) -> ProtoCoin {
104 ProtoCoin {
105 denom: coin.denom.to_string(),
106 amount: coin.amount.to_string(),
107 }
108 }
109}
110
111impl From<BaseCoin> for PrefixedCoin {
112 fn from(coin: BaseCoin) -> PrefixedCoin {
113 PrefixedCoin {
114 denom: coin.denom.into(),
115 amount: coin.amount,
116 }
117 }
118}
119
120impl<D: Display> Display for Coin<D> {
121 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
122 write!(f, "{}{}", self.amount, self.denom)
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[test]
131 fn test_parse_raw_coin() -> Result<(), Error> {
132 {
133 let coin = RawCoin::from_str("123stake")?;
134 assert_eq!(coin.denom, "stake");
135 assert_eq!(coin.amount, 123u64.into());
136 }
137
138 {
139 let coin = RawCoin::from_str("1 ab1")?;
140 assert_eq!(coin.denom, "ab1");
141 assert_eq!(coin.amount, 1u64.into());
142 }
143
144 {
145 let coin = RawCoin::from_str("0x1/:._-")?;
146 assert_eq!(coin.denom, "x1/:._-");
147 assert_eq!(coin.amount, 0u64.into());
148 }
149
150 {
151 let res = RawCoin::from_str("0x!");
153 assert!(res.is_err());
154 }
155
156 Ok(())
157 }
158
159 #[test]
160 fn test_parse_raw_coin_list() -> Result<(), Error> {
161 {
162 let coins = RawCoin::from_string_list("123stake,1ab1,999de-n0m")?;
163 assert_eq!(coins.len(), 3);
164
165 assert_eq!(coins[0].denom, "stake");
166 assert_eq!(coins[0].amount, 123u64.into());
167
168 assert_eq!(coins[1].denom, "ab1");
169 assert_eq!(coins[1].amount, 1u64.into());
170
171 assert_eq!(coins[2].denom, "de-n0m");
172 assert_eq!(coins[2].amount, 999u64.into());
173 }
174
175 Ok(())
176 }
177}