lichen_client_sdk/
types.rs1use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::str::FromStr;
6
7const SPORES_PER_LICN: u64 = 1_000_000_000;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum BalanceParseError {
11 Empty,
12 Negative,
13 InvalidFormat,
14 TooManyFractionalDigits,
15 Overflow,
16}
17
18impl fmt::Display for BalanceParseError {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 Self::Empty => write!(f, "LICN amount cannot be empty"),
22 Self::Negative => write!(f, "LICN amount cannot be negative"),
23 Self::InvalidFormat => write!(f, "LICN amount must be a decimal string"),
24 Self::TooManyFractionalDigits => {
25 write!(f, "LICN amount supports at most 9 fractional digits")
26 }
27 Self::Overflow => write!(f, "LICN amount exceeds supported range"),
28 }
29 }
30}
31
32impl std::error::Error for BalanceParseError {}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct Balance {
37 spores: u64,
38}
39
40impl Balance {
41 pub fn from_spores(spores: u64) -> Self {
43 Self { spores }
44 }
45
46 pub fn from_licn(licn: &str) -> Result<Self, BalanceParseError> {
48 let licn = licn.trim();
49 if licn.is_empty() {
50 return Err(BalanceParseError::Empty);
51 }
52
53 if licn.starts_with('-') {
54 return Err(BalanceParseError::Negative);
55 }
56
57 let licn = licn.strip_prefix('+').unwrap_or(licn);
58 let mut parts = licn.split('.');
59 let whole_part = parts.next().unwrap_or_default();
60 let frac_part = parts.next();
61 if parts.next().is_some() {
62 return Err(BalanceParseError::InvalidFormat);
63 }
64
65 if whole_part.is_empty() && frac_part.unwrap_or_default().is_empty() {
66 return Err(BalanceParseError::InvalidFormat);
67 }
68
69 if !whole_part.chars().all(|ch| ch.is_ascii_digit()) {
70 return Err(BalanceParseError::InvalidFormat);
71 }
72
73 let whole_spores = if whole_part.is_empty() {
74 0
75 } else {
76 whole_part
77 .parse::<u64>()
78 .map_err(|_| BalanceParseError::Overflow)?
79 .checked_mul(SPORES_PER_LICN)
80 .ok_or(BalanceParseError::Overflow)?
81 };
82
83 let frac_spores = if let Some(frac_part) = frac_part {
84 if !frac_part.chars().all(|ch| ch.is_ascii_digit()) {
85 return Err(BalanceParseError::InvalidFormat);
86 }
87 if frac_part.len() > 9 {
88 return Err(BalanceParseError::TooManyFractionalDigits);
89 }
90 if frac_part.is_empty() {
91 0
92 } else {
93 let frac_digits = frac_part
94 .parse::<u64>()
95 .map_err(|_| BalanceParseError::Overflow)?;
96 frac_digits
97 .checked_mul(10_u64.pow((9 - frac_part.len()) as u32))
98 .ok_or(BalanceParseError::Overflow)?
99 }
100 } else {
101 0
102 };
103
104 Ok(Self {
105 spores: whole_spores
106 .checked_add(frac_spores)
107 .ok_or(BalanceParseError::Overflow)?,
108 })
109 }
110
111 pub fn from_licn_parts(
112 whole_licn: u64,
113 fractional_spores: u32,
114 ) -> Result<Self, BalanceParseError> {
115 if fractional_spores >= SPORES_PER_LICN as u32 {
116 return Err(BalanceParseError::TooManyFractionalDigits);
117 }
118
119 let whole_spores = whole_licn
120 .checked_mul(SPORES_PER_LICN)
121 .ok_or(BalanceParseError::Overflow)?;
122
123 Ok(Self {
124 spores: whole_spores
125 .checked_add(fractional_spores as u64)
126 .ok_or(BalanceParseError::Overflow)?,
127 })
128 }
129
130 pub fn spores(&self) -> u64 {
132 self.spores
133 }
134
135 pub fn licn(&self) -> f64 {
137 self.spores as f64 / SPORES_PER_LICN as f64
138 }
139}
140
141impl FromStr for Balance {
142 type Err = BalanceParseError;
143
144 fn from_str(s: &str) -> Result<Self, Self::Err> {
145 Self::from_licn(s)
146 }
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct Block {
152 pub hash: String,
153 pub parent_hash: String,
154 pub slot: u64,
155 pub state_root: String,
156 pub timestamp: u64,
157 pub transaction_count: u64,
158 pub validator: String,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct NetworkInfo {
164 pub chain_id: String,
165 pub current_slot: u64,
166 pub network_id: String,
167 pub peer_count: u64,
168 pub validator_count: u64,
169 pub version: String,
170}
171
172pub use lichen_core::Transaction;
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn test_balance_roundtrip() {
181 let b = Balance::from_spores(1_500_000_000);
182 assert_eq!(b.spores(), 1_500_000_000);
183 assert!((b.licn() - 1.5).abs() < 1e-9);
184 }
185
186 #[test]
187 fn test_balance_from_licn_normal() {
188 let b = Balance::from_licn("2.5").unwrap();
189 assert_eq!(b.spores(), 2_500_000_000);
190 }
191
192 #[test]
193 fn test_balance_from_licn_negative() {
194 assert_eq!(
195 Balance::from_licn("-1.0").unwrap_err(),
196 BalanceParseError::Negative
197 );
198 }
199
200 #[test]
201 fn test_balance_from_licn_invalid_format() {
202 assert_eq!(
203 Balance::from_licn("NaN").unwrap_err(),
204 BalanceParseError::InvalidFormat
205 );
206 }
207
208 #[test]
209 fn test_balance_from_licn_overflow() {
210 assert_eq!(
211 Balance::from_licn("18446744074").unwrap_err(),
212 BalanceParseError::Overflow
213 );
214 }
215
216 #[test]
217 fn test_balance_from_licn_zero() {
218 let b = Balance::from_licn("0").unwrap();
219 assert_eq!(b.spores(), 0);
220 }
221
222 #[test]
223 fn test_balance_empty_amount() {
224 assert_eq!(
225 Balance::from_licn(" ").unwrap_err(),
226 BalanceParseError::Empty
227 );
228 }
229
230 #[test]
231 fn test_balance_from_licn_tiny_fraction() {
232 let b = Balance::from_licn("0.000000001").unwrap();
233 assert_eq!(b.spores(), 1);
234 }
235
236 #[test]
237 fn test_balance_from_licn_sub_spore() {
238 assert_eq!(
239 Balance::from_licn("0.0000000001").unwrap_err(),
240 BalanceParseError::TooManyFractionalDigits
241 );
242 }
243
244 #[test]
245 fn test_balance_from_licn_leading_decimal() {
246 let b = Balance::from_licn(".5").unwrap();
247 assert_eq!(b.spores(), 500_000_000);
248 }
249
250 #[test]
251 fn test_balance_from_licn_trailing_decimal() {
252 let b = Balance::from_licn("1.").unwrap();
253 assert_eq!(b.spores(), 1_000_000_000);
254 }
255
256 #[test]
257 fn test_balance_from_licn_parts() {
258 let b = Balance::from_licn_parts(12, 345_000_000).unwrap();
259 assert_eq!(b.spores(), 12_345_000_000);
260 }
261
262 #[test]
263 fn test_balance_from_spores_max() {
264 let b = Balance::from_spores(u64::MAX);
265 assert_eq!(b.spores(), u64::MAX);
266 }
267
268 #[test]
269 fn test_balance_eq() {
270 let a = Balance::from_spores(100);
271 let b = Balance::from_spores(100);
272 assert_eq!(a, b);
273 }
274
275 #[test]
276 fn test_balance_copy() {
277 let a = Balance::from_spores(42);
278 let b = a;
279 assert_eq!(a.spores(), b.spores());
280 }
281
282 #[test]
283 fn test_balance_debug() {
284 let b = Balance::from_spores(0);
285 let s = format!("{:?}", b);
286 assert!(s.contains("Balance"));
287 }
288
289 #[test]
290 fn test_balance_licn_precision() {
291 let b = Balance::from_licn("1.0").unwrap();
293 assert_eq!(b.spores(), 1_000_000_000);
294 assert!((b.licn() - 1.0).abs() < 1e-15);
295 }
296}