Skip to main content

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