1use std::fmt;
9use std::str::FromStr;
10
11use serde::{Deserialize, Serialize};
12
13use crate::error::{Error, Result};
14
15const WEBCASH_SYMBOL_BYTES: usize = 3;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
20pub struct Amount {
21 pub wats: i64,
23}
24
25impl Amount {
26 pub const DECIMALS: u32 = 8;
28
29 pub const UNIT: i64 = 10_i64.pow(Self::DECIMALS);
31
32 pub const ZERO: Amount = Amount { wats: 0 };
34
35 pub const fn from_wats(wats: i64) -> Self {
37 Amount { wats }
38 }
39
40 pub const fn from_sats(wats: i64) -> Self {
42 Amount { wats }
43 }
44
45 fn parse_scientific_notation(s: &str) -> Result<Self> {
48 let parts: Vec<&str> = s.split(&['E', 'e'][..]).collect();
49 if parts.len() != 2 {
50 return Err(Error::amount("invalid scientific notation format"));
51 }
52
53 let coefficient: f64 = parts[0]
54 .parse()
55 .map_err(|_| Error::amount("invalid coefficient in scientific notation"))?;
56 let exponent: i32 = parts[1]
57 .parse()
58 .map_err(|_| Error::amount("invalid exponent in scientific notation"))?;
59
60 let result = if exponent >= 0 {
61 coefficient * 10_f64.powi(exponent)
63 } else {
64 coefficient / 10_f64.powi(-exponent)
66 };
67
68 Self::from_webcash(result)
69 }
70
71 pub fn from_webcash(webcash: f64) -> Result<Self> {
73 if webcash < 0.0 {
74 return Err(Error::amount("negative amounts not allowed"));
75 }
76
77 let wats = (webcash * Self::UNIT as f64).round() as i64;
79
80 if wats < 0 {
82 return Err(Error::amount("amount too large"));
83 }
84
85 Ok(Amount { wats })
86 }
87
88 pub fn to_decimal_string(&self) -> String {
91 self.to_string_with_decimals(Self::DECIMALS)
92 }
93
94 pub fn to_string_with_decimals(&self, decimals: u32) -> String {
96 if self.wats == 0 {
97 return "0".to_string();
98 }
99
100 let divisor = 10_i64.pow(decimals);
101 let integer_part = self.wats / divisor;
102 let fractional_part = (self.wats % divisor).abs();
103
104 if fractional_part == 0 {
105 format!("{}", integer_part)
106 } else {
107 let fractional_str = format!("{:0width$}", fractional_part, width = decimals as usize);
108 let trimmed = fractional_str.trim_end_matches('0');
109 if trimmed.is_empty() {
110 format!("{}", integer_part)
111 } else {
112 format!("{}.{}", integer_part, trimmed)
113 }
114 }
115 }
116
117 pub fn to_webcash(&self) -> f64 {
119 self.wats as f64 / Self::UNIT as f64
120 }
121
122 pub fn to_wats_string(&self) -> String {
125 self.wats.to_string()
126 }
127
128 pub fn is_valid(&self) -> bool {
130 self.wats >= 0
131 }
132
133 pub fn is_zero(&self) -> bool {
135 self.wats == 0
136 }
137
138 pub fn is_positive(&self) -> bool {
140 self.wats > 0
141 }
142
143 pub fn is_negative(&self) -> bool {
145 self.wats < 0
146 }
147
148 pub fn abs(&self) -> Self {
150 Amount {
151 wats: self.wats.abs(),
152 }
153 }
154
155 pub fn saturating_add(&self, other: &Amount) -> Amount {
157 Amount {
158 wats: self.wats.saturating_add(other.wats),
159 }
160 }
161
162 pub fn saturating_sub(&self, other: &Amount) -> Amount {
164 Amount {
165 wats: self.wats.saturating_sub(other.wats),
166 }
167 }
168
169 pub fn checked_add(&self, other: &Amount) -> Option<Amount> {
171 self.wats
172 .checked_add(other.wats)
173 .map(|wats| Amount { wats })
174 }
175
176 pub fn checked_sub(&self, other: &Amount) -> Option<Amount> {
178 self.wats
179 .checked_sub(other.wats)
180 .map(|wats| Amount { wats })
181 }
182
183 pub fn checked_mul(&self, other: i64) -> Option<Amount> {
185 self.wats.checked_mul(other).map(|wats| Amount { wats })
186 }
187
188 pub fn checked_div(&self, other: i64) -> Option<Amount> {
190 self.wats.checked_div(other).map(|wats| Amount { wats })
191 }
192}
193
194impl Default for Amount {
195 fn default() -> Self {
196 Amount::ZERO
197 }
198}
199
200impl fmt::Display for Amount {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 write!(f, "{}", self.to_decimal_string())
203 }
204}
205
206impl FromStr for Amount {
207 type Err = Error;
208
209 fn from_str(s: &str) -> Result<Self> {
210 if s.is_empty() {
212 return Err(Error::amount("empty string"));
213 }
214
215 if s.contains('E') || s.contains('e') {
217 return Self::parse_scientific_notation(s);
218 }
219
220 let s = if let Some(stripped) = s.strip_prefix('e') {
222 stripped
223 } else if s.starts_with('₩') {
224 &s[WEBCASH_SYMBOL_BYTES..]
225 } else {
226 s
227 };
228
229 if s == "0" {
231 return Ok(Amount::ZERO);
232 }
233
234 let parts: Vec<&str> = s.split('.').collect();
236 if parts.len() > 2 {
237 return Err(Error::amount("too many decimal points"));
238 }
239
240 let integer_part = parts[0];
241 let fractional_part = if parts.len() == 2 { parts[1] } else { "" };
242
243 if integer_part.is_empty() && !fractional_part.is_empty() {
245 return Err(Error::amount("missing integer part"));
246 }
247
248 let mut wats = if integer_part.is_empty() {
250 0
251 } else {
252 integer_part
253 .parse::<i64>()
254 .map_err(|_| Error::amount("invalid integer part"))?
255 };
256
257 if !fractional_part.is_empty() {
259 if fractional_part.len() > Amount::DECIMALS as usize {
260 return Err(Error::amount("too many decimal places"));
261 }
262
263 let frac_value = fractional_part
265 .parse::<i64>()
266 .map_err(|_| Error::amount("invalid fractional part"))?;
267
268 let multiplier = 10_i64.pow(Amount::DECIMALS - fractional_part.len() as u32);
270 let fractional_sats = frac_value * multiplier;
271
272 wats = wats
274 .checked_mul(Amount::UNIT)
275 .and_then(|s| s.checked_add(fractional_sats))
276 .ok_or_else(|| Error::amount("amount too large"))?;
277 } else {
278 wats = wats
280 .checked_mul(Amount::UNIT)
281 .ok_or_else(|| Error::amount("amount too large"))?;
282 }
283
284 if wats < 0 {
286 return Err(Error::amount("negative amounts not allowed"));
287 }
288
289 Ok(Amount { wats })
290 }
291}
292
293impl std::ops::Add for Amount {
294 type Output = Amount;
295
296 fn add(self, other: Amount) -> Amount {
297 Amount {
298 wats: self.wats.saturating_add(other.wats),
299 }
300 }
301}
302
303impl std::ops::Sub for Amount {
304 type Output = Amount;
305
306 fn sub(self, other: Amount) -> Amount {
307 Amount {
308 wats: self.wats.saturating_sub(other.wats),
309 }
310 }
311}
312
313impl std::ops::Mul<i64> for Amount {
314 type Output = Amount;
315
316 fn mul(self, rhs: i64) -> Amount {
317 Amount {
318 wats: self.wats.saturating_mul(rhs),
319 }
320 }
321}
322
323impl std::ops::Div<i64> for Amount {
324 type Output = Amount;
325
326 fn div(self, rhs: i64) -> Amount {
331 Amount {
332 wats: self.wats / rhs, }
334 }
335}
336
337impl std::ops::AddAssign for Amount {
338 fn add_assign(&mut self, other: Amount) {
339 self.wats = self.wats.saturating_add(other.wats);
340 }
341}
342
343impl std::ops::SubAssign for Amount {
344 fn sub_assign(&mut self, other: Amount) {
345 self.wats = self.wats.saturating_sub(other.wats);
346 }
347}