zebra_rpc/methods/types/
zec.rs

1//! ZEC amount formatting.
2//!
3//! The `f64` values returned by this type should not be used in consensus-critical code.
4//! The values themselves are accurate, but any calculations using them could be lossy.
5
6use std::{
7    fmt,
8    hash::{Hash, Hasher},
9    ops,
10    str::FromStr,
11};
12
13use zebra_chain::amount::{self, Amount, Constraint, COIN};
14
15use zebra_node_services::BoxError;
16
17// Doc links only
18#[allow(unused_imports)]
19use zebra_chain::amount::MAX_MONEY;
20
21/// The maximum precision of a zatoshi in ZEC.
22/// Also used as the default decimal precision for ZEC formatting.
23///
24/// This is the same as the `getblocksubsidy` RPC in `zcashd`:
25/// <https://github.com/zcash/zcash/blob/f6a4f68115ea4c58d55c8538579d0877ba9c8f79/src/rpc/server.cpp#L134>
26pub const MAX_ZEC_FORMAT_PRECISION: usize = 8;
27
28/// A wrapper type that formats [`Amount`]s as ZEC, using double-precision floating point.
29///
30/// This formatting is accurate to the nearest zatoshi, as long as the number of floating-point
31/// calculations is very small. This is because [`MAX_MONEY`] uses 51 bits, but [`f64`] has
32/// [53 bits of precision](f64::MANTISSA_DIGITS).
33///
34/// Rust uses [`roundTiesToEven`](f32), which can lose one bit of precision per calculation
35/// in the worst case. (Assuming the platform implements it correctly.)
36///
37/// Unlike `zcashd`, Zebra doesn't have control over its JSON number precision,
38/// because it uses `serde_json`'s formatter. But `zcashd` uses a fixed-point calculation:
39/// <https://github.com/zcash/zcash/blob/f6a4f68115ea4c58d55c8538579d0877ba9c8f79/src/rpc/server.cpp#L134>
40#[derive(Clone, Copy, serde::Serialize, serde::Deserialize, Default)]
41#[serde(try_from = "f64")]
42#[serde(into = "f64")]
43#[serde(bound = "C: Constraint + Clone")]
44pub struct Zec<C: Constraint>(Amount<C>);
45
46impl<C: Constraint> Zec<C> {
47    /// Returns the `f64` ZEC value for the inner amount.
48    ///
49    /// The returned value should not be used for consensus-critical calculations,
50    /// because it is lossy.
51    pub fn lossy_zec(&self) -> f64 {
52        let zats = self.zatoshis();
53        // These conversions are exact, because f64 has 53 bits of precision,
54        // MAX_MONEY has <51, and COIN has <27, so we have 2 extra bits of precision.
55        let zats = zats as f64;
56        let coin = COIN as f64;
57
58        // After this calculation, we might have lost one bit of precision,
59        // leaving us with only 1 extra bit.
60        zats / coin
61    }
62
63    /// Converts a `f64` ZEC value to a [`Zec`] amount.
64    ///
65    /// This method should not be used for consensus-critical calculations, because it is lossy.
66    pub fn from_lossy_zec(lossy_zec: f64) -> Result<Self, BoxError> {
67        // This conversion is exact, because f64 has 53 bits of precision, but COIN has <27
68        let coin = COIN as f64;
69
70        // After this calculation, we might have lost one bit of precision
71        let zats = lossy_zec * coin;
72
73        if zats != zats.trunc() {
74            return Err(
75                "loss of precision parsing ZEC value: floating point had fractional zatoshis"
76                    .into(),
77            );
78        }
79
80        // We know this conversion is exact, because we just checked.
81        let zats = zats as i64;
82        let zats = Amount::try_from(zats)?;
83
84        Ok(Self(zats))
85    }
86}
87
88// These conversions are lossy, so they should not be used in consensus-critical code
89impl<C: Constraint> From<Zec<C>> for f64 {
90    fn from(zec: Zec<C>) -> f64 {
91        zec.lossy_zec()
92    }
93}
94
95impl<C: Constraint> TryFrom<f64> for Zec<C> {
96    type Error = BoxError;
97
98    fn try_from(value: f64) -> Result<Self, Self::Error> {
99        Self::from_lossy_zec(value)
100    }
101}
102
103// This formatter should not be used for consensus-critical outputs.
104impl<C: Constraint> fmt::Display for Zec<C> {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        let zec = self.lossy_zec();
107
108        // Try to format like `zcashd` by default
109        let decimals = f.precision().unwrap_or(MAX_ZEC_FORMAT_PRECISION);
110        let string = format!("{zec:.decimals$}");
111        f.pad_integral(zec >= 0.0, "", &string)
112    }
113}
114
115// This parser should not be used for consensus-critical inputs.
116impl<C: Constraint> FromStr for Zec<C> {
117    type Err = BoxError;
118
119    fn from_str(s: &str) -> Result<Self, Self::Err> {
120        let lossy_zec: f64 = s.parse()?;
121
122        Self::from_lossy_zec(lossy_zec)
123    }
124}
125
126impl<C: Constraint> std::fmt::Debug for Zec<C> {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        f.debug_struct(&format!("Zec<{}>", std::any::type_name::<C>()))
129            .field("ZEC", &self.to_string())
130            .field("zat", &self.0)
131            .finish()
132    }
133}
134
135impl<C: Constraint> ops::Deref for Zec<C> {
136    type Target = Amount<C>;
137
138    fn deref(&self) -> &Self::Target {
139        &self.0
140    }
141}
142
143impl<C: Constraint> ops::DerefMut for Zec<C> {
144    fn deref_mut(&mut self) -> &mut Self::Target {
145        &mut self.0
146    }
147}
148
149impl<C: Constraint> From<Amount<C>> for Zec<C> {
150    fn from(amount: Amount<C>) -> Self {
151        Self(amount)
152    }
153}
154
155impl<C: Constraint> From<Zec<C>> for Amount<C> {
156    fn from(zec: Zec<C>) -> Amount<C> {
157        zec.0
158    }
159}
160
161impl<C: Constraint> From<Zec<C>> for i64 {
162    fn from(zec: Zec<C>) -> i64 {
163        zec.0.into()
164    }
165}
166
167impl<C: Constraint> TryFrom<i64> for Zec<C> {
168    type Error = amount::Error;
169
170    fn try_from(value: i64) -> Result<Self, Self::Error> {
171        Ok(Self(Amount::try_from(value)?))
172    }
173}
174
175impl<C: Constraint> Hash for Zec<C> {
176    /// Zecs with the same value are equal, even if they have different constraints
177    fn hash<H: Hasher>(&self, state: &mut H) {
178        self.0.hash(state);
179    }
180}
181
182impl<C1: Constraint, C2: Constraint> PartialEq<Zec<C2>> for Zec<C1> {
183    fn eq(&self, other: &Zec<C2>) -> bool {
184        self.0.eq(&other.0)
185    }
186}
187
188impl<C: Constraint> PartialEq<i64> for Zec<C> {
189    fn eq(&self, other: &i64) -> bool {
190        self.0.eq(other)
191    }
192}
193
194impl<C: Constraint> PartialEq<Zec<C>> for i64 {
195    fn eq(&self, other: &Zec<C>) -> bool {
196        self.eq(&other.0)
197    }
198}
199
200impl<C1: Constraint, C2: Constraint> PartialEq<Amount<C2>> for Zec<C1> {
201    fn eq(&self, other: &Amount<C2>) -> bool {
202        self.0.eq(other)
203    }
204}
205
206impl<C1: Constraint, C2: Constraint> PartialEq<Zec<C2>> for Amount<C1> {
207    fn eq(&self, other: &Zec<C2>) -> bool {
208        self.eq(&other.0)
209    }
210}
211
212impl<C: Constraint> Eq for Zec<C> {}