celestia_types/state/
coin.rs1use 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#[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 denom: String,
19 amount: u64,
21}
22
23impl Coin {
24 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 pub fn utia(amount: u64) -> Self {
36 Coin::new("utia", amount).expect("denom is always valid")
37 }
38
39 pub fn amount(&self) -> u64 {
41 self.amount
42 }
43
44 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 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 if !matches!(chars.next(), Some('a'..='z' | 'A'..='Z')) {
87 return Err(Error::InvalidCoinDenomination(denom.to_owned()));
88 }
89
90 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 #[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}