ibc_relayer_types/applications/transfer/
coin.rs

1use 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
15/// A `Coin` type with fully qualified `PrefixedDenom`.
16pub type PrefixedCoin = Coin<PrefixedDenom>;
17
18/// A `Coin` type with an unprefixed denomination.
19pub type BaseCoin = Coin<BaseDenom>;
20
21pub type RawCoin = Coin<String>;
22
23/// Coin defines a token with a denomination and an amount.
24#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
25pub struct Coin<D> {
26    /// Denomination
27    pub denom: D,
28    /// Amount
29    #[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    // #[allow(clippy::assign_op_pattern)]
68    fn from_str(coin_str: &str) -> Result<Self, Error> {
69        // Denominations can be 3 ~ 128 characters long and support letters, followed by either
70        // a letter, a number or a separator ('/', ':', '.', '_' or '-').
71        // Loosely copy the regex from here:
72        // https://github.com/cosmos/cosmos-sdk/blob/v0.45.5/types/coin.go#L760-L762
73        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            // `!` is not allowed
152            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}