Skip to main content

celestia_types/state/
coin.rs

1use celestia_proto::cosmos::base::v1beta1::Coin as RawCoin;
2use serde::{Deserialize, Serialize};
3#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
4use wasm_bindgen::prelude::*;
5
6use crate::{Error, Result};
7
8/// Coin defines a token with a denomination and an amount.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(try_from = "RawCoin", into = "RawCoin")]
11#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
12#[cfg_attr(
13    all(target_arch = "wasm32", feature = "wasm-bindgen"),
14    wasm_bindgen(getter_with_clone)
15)]
16pub struct Coin {
17    /// Coin denomination
18    denom: String,
19    /// Coin amount
20    amount: u64,
21}
22
23impl Coin {
24    /// Create a new coin with geven amount and denomination.
25    pub fn new(denom: &str, amount: u64) -> Result<Coin> {
26        validate_denom(denom)?;
27
28        Ok(Coin {
29            denom: denom.to_owned(),
30            amount,
31        })
32    }
33
34    /// Create a coin with given amount of `utia`.
35    pub fn utia(amount: u64) -> Self {
36        Coin::new("utia", amount).expect("denom is always valid")
37    }
38
39    /// Amount getter.
40    pub fn amount(&self) -> u64 {
41        self.amount
42    }
43
44    /// Denomination getter.
45    pub fn denom(&self) -> &str {
46        self.denom.as_str()
47    }
48}
49
50impl TryFrom<RawCoin> for Coin {
51    type Error = Error;
52
53    fn try_from(value: RawCoin) -> Result<Self, Self::Error> {
54        validate_denom(&value.denom)?;
55
56        let amount = value
57            .amount
58            .parse()
59            .map_err(|_| Error::InvalidCoinAmount(value.amount))?;
60
61        Ok(Coin {
62            denom: value.denom,
63            amount,
64        })
65    }
66}
67
68impl From<Coin> for RawCoin {
69    fn from(value: Coin) -> Self {
70        RawCoin {
71            denom: value.denom,
72            amount: value.amount.to_string(),
73        }
74    }
75}
76
77fn validate_denom(denom: &str) -> Result<()> {
78    // Length must be 3-128 characters
79    if denom.len() < 3 || denom.len() > 128 {
80        return Err(Error::InvalidCoinDenomination(denom.to_owned()));
81    }
82
83    let mut chars = denom.chars();
84
85    // First character must be a letter
86    if !matches!(chars.next(), Some('a'..='z' | 'A'..='Z')) {
87        return Err(Error::InvalidCoinDenomination(denom.to_owned()));
88    }
89
90    // The rest can be a letter, a number, or a symbol from '/', ':', '.', '_', '-'
91    if chars.all(|c| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '/' | ':' | '.' | '_' | '-')) {
92        Ok(())
93    } else {
94        Err(Error::InvalidCoinDenomination(denom.to_owned()))
95    }
96}
97
98#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
99pub use wbg::*;
100
101#[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))]
102mod wbg {
103    use super::Coin;
104    use js_sys::BigInt;
105    use wasm_bindgen::prelude::*;
106
107    use lumina_utils::make_object;
108
109    #[wasm_bindgen(typescript_custom_section)]
110    const _: &str = "
111    /**
112     * Coin
113     */
114    export interface Coin {
115      denom: string,
116      amount: bigint
117    }
118    ";
119
120    #[wasm_bindgen]
121    extern "C" {
122        /// Coin exposed to javascript
123        #[wasm_bindgen(typescript_type = "Coin")]
124        pub type JsCoin;
125    }
126
127    impl From<Coin> for JsCoin {
128        fn from(value: Coin) -> JsCoin {
129            let obj = make_object!(
130                "denom" => value.denom().into(),
131                "amount" => BigInt::from(value.amount())
132            );
133
134            obj.unchecked_into()
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[cfg(target_arch = "wasm32")]
144    use wasm_bindgen_test::wasm_bindgen_test as test;
145
146    #[test]
147    fn deserialize_coin() {
148        let s = r#"{"denom":"abcd","amount":"1234"}"#;
149        let coin: Coin = serde_json::from_str(s).unwrap();
150        assert_eq!(coin.denom(), "abcd");
151        assert_eq!(coin.amount(), 1234);
152    }
153
154    #[test]
155    fn deserialize_invalid_denom() {
156        let s = r#"{"denom":"0asdadas","amount":"1234"}"#;
157        serde_json::from_str::<Coin>(s).unwrap_err();
158    }
159
160    #[test]
161    fn deserialize_invalid_amount() {
162        let s = r#"{"denom":"abcd","amount":"a1234"}"#;
163        serde_json::from_str::<Coin>(s).unwrap_err();
164    }
165
166    #[test]
167    fn serialize_coin() {
168        let coin = Coin::new("abcd", 1234).unwrap();
169        let s = serde_json::to_string(&coin).unwrap();
170        let expected = r#"{"denom":"abcd","amount":"1234"}"#;
171        assert_eq!(s, expected);
172    }
173
174    #[test]
175    fn invalid_coin_denom() {
176        Coin::new("0bc", 1234).unwrap_err();
177    }
178
179    #[test]
180    fn valid_denom() {
181        validate_denom("abc").unwrap();
182        validate_denom("a01").unwrap();
183        validate_denom("A01").unwrap();
184        validate_denom("z01").unwrap();
185        validate_denom("Z01").unwrap();
186        validate_denom("aAzZ09/:._-").unwrap();
187    }
188
189    #[test]
190    fn small_denom() {
191        validate_denom("aa").unwrap_err();
192        validate_denom("aaa").unwrap();
193    }
194
195    #[test]
196    fn large_denom() {
197        validate_denom(&"a".repeat(128)).unwrap();
198        validate_denom(&"a".repeat(129)).unwrap_err();
199    }
200
201    #[test]
202    fn demon_starting_with_number() {
203        validate_denom("0bc").unwrap_err();
204    }
205
206    #[test]
207    fn demon_starting_with_symbol() {
208        validate_denom("_bc").unwrap_err();
209    }
210
211    #[test]
212    fn demon_invalid_symbol() {
213        validate_denom("abc$").unwrap_err();
214    }
215}