Skip to main content

cuprate_rpc_types/misc/
distribution.rs

1//! Output distributions for [`crate::json::GetOutputDistributionResponse`].
2
3//---------------------------------------------------------------------------------------------------- Use
4#[cfg(any(feature = "epee", feature = "serde"))]
5use monero_oxide::io::VarInt;
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9#[cfg(feature = "epee")]
10use cuprate_epee_encoding::{
11    container_as_blob::ContainerAsBlob,
12    epee_object, error,
13    macros::bytes::{Buf, BufMut},
14    read_epee_value, read_marker, write_field, EpeeObject, EpeeObjectBuilder, EpeeValue,
15};
16
17//---------------------------------------------------------------------------------------------------- Free
18/// Used for [`Distribution::CompressedBinary::distribution`].
19#[doc = crate::macros::monero_definition_link!(
20    "cc73fe71162d564ffda8e549b79a350bca53c454",
21    "rpc/core_rpc_server_commands_defs.h",
22    45..=55
23)]
24#[cfg(any(feature = "epee", feature = "serde"))]
25fn compress_integer_array(v: &[u64]) -> Vec<u8> {
26    let mut out = Vec::with_capacity(v.len() * 2);
27    for val in v {
28        VarInt::write(val, &mut out).expect("Writing to vec should not fail");
29    }
30    out
31}
32
33/// Used for [`Distribution::CompressedBinary::distribution`].
34#[doc = crate::macros::monero_definition_link!(
35    "cc73fe71162d564ffda8e549b79a350bca53c454",
36    "rpc/core_rpc_server_commands_defs.h",
37    57..=72
38)]
39#[cfg(any(feature = "epee", feature = "serde"))]
40fn decompress_integer_array(mut s: &[u8]) -> std::io::Result<Vec<u64>> {
41    let mut v = Vec::new();
42    while !s.is_empty() {
43        v.push(VarInt::read(&mut s)?);
44    }
45    Ok(v)
46}
47
48//---------------------------------------------------------------------------------------------------- Distribution
49#[doc = crate::macros::monero_definition_link!(
50    "cc73fe71162d564ffda8e549b79a350bca53c454",
51    "rpc/core_rpc_server_commands_defs.h",
52    2468..=2508
53)]
54/// Used in [`crate::json::GetOutputDistributionResponse`].
55///
56/// # Internals
57/// This type's (de)serialization depends on `monerod`'s (de)serialization.
58///
59/// During serialization:
60/// [`Self::Uncompressed`] will emit:
61/// - `compress: false`
62///
63/// [`Self::CompressedBinary`] will emit:
64/// - `binary: true`
65/// - `compress: true`
66///
67/// Upon deserialization, the presence of a `compressed_data`
68/// field signifies that the [`Self::CompressedBinary`] should
69/// be selected.
70#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
71#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
72#[cfg_attr(feature = "serde", serde(untagged))]
73pub enum Distribution {
74    /// Distribution data will be (de)serialized as either JSON or binary (uncompressed).
75    Uncompressed(DistributionUncompressed),
76    /// Distribution data will be (de)serialized as compressed binary.
77    CompressedBinary(DistributionCompressedBinary),
78}
79
80impl Default for Distribution {
81    fn default() -> Self {
82        Self::Uncompressed(DistributionUncompressed::default())
83    }
84}
85
86/// Data within [`Distribution::Uncompressed`].
87#[cfg_attr(feature = "serde", derive(Deserialize))]
88#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
89pub struct DistributionUncompressed {
90    pub start_height: u64,
91    pub base: u64,
92    /// TODO: this is a binary JSON string if `binary == true`.
93    pub distribution: Vec<u64>,
94    pub amount: u64,
95    pub binary: bool,
96}
97
98// Manual `Serialize` so the JSON output includes `compress: false`, matching
99// monerod. `derive(Serialize)` would only emit the struct fields.
100#[cfg(feature = "serde")]
101impl Serialize for DistributionUncompressed {
102    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
103    where
104        S: serde::Serializer,
105    {
106        use serde::ser::SerializeStruct;
107        let mut s = serializer.serialize_struct("DistributionUncompressed", 6)?;
108        s.serialize_field("start_height", &self.start_height)?;
109        s.serialize_field("base", &self.base)?;
110        s.serialize_field("distribution", &self.distribution)?;
111        s.serialize_field("amount", &self.amount)?;
112        s.serialize_field("binary", &self.binary)?;
113        s.serialize_field("compress", &false)?;
114        s.end()
115    }
116}
117
118// `DistributionUncompressed` is never used as a standalone EPEE object.
119// Its `EpeeObject` impl exists only so that `Distribution::number_of_fields()`
120// can call `s.number_of_fields()`. The write path is handled manually in
121// `Distribution::write_fields` to correctly switch between blob and array
122// encoding based on the `binary` flag — do NOT add a standalone write impl here.
123#[cfg(feature = "epee")]
124#[derive(Default)]
125pub struct __DistributionUncompressedEpeeBuilder;
126
127#[cfg(feature = "epee")]
128impl EpeeObjectBuilder<DistributionUncompressed> for __DistributionUncompressedEpeeBuilder {
129    fn add_field<B: Buf>(&mut self, _: &str, _: &mut B) -> error::Result<bool> {
130        unreachable!("DistributionUncompressed is never deserialized as a standalone EPEE object")
131    }
132
133    fn finish(self) -> error::Result<DistributionUncompressed> {
134        unreachable!("DistributionUncompressed is never deserialized as a standalone EPEE object")
135    }
136}
137
138#[cfg(feature = "epee")]
139impl EpeeObject for DistributionUncompressed {
140    type Builder = __DistributionUncompressedEpeeBuilder;
141
142    fn number_of_fields(&self) -> u64 {
143        4 + u64::from(EpeeValue::should_write(&self.distribution))
144    }
145
146    fn write_fields<B: BufMut>(self, _: &mut B) -> error::Result<()> {
147        unreachable!() // We don't write directly here, we do it in [`Distribution`]
148    }
149}
150
151/// Data within [`Distribution::CompressedBinary`].
152#[cfg_attr(feature = "serde", derive(Deserialize))]
153#[derive(Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
154pub struct DistributionCompressedBinary {
155    pub start_height: u64,
156    pub base: u64,
157    #[cfg_attr(
158        feature = "serde",
159        serde(deserialize_with = "deserialize_compressed_data_as_distribution")
160    )]
161    #[cfg_attr(feature = "serde", serde(rename = "compressed_data"))]
162    pub distribution: Vec<u64>,
163    pub amount: u64,
164}
165
166// monerod includes `binary: true` and `compress: true`. `derive(Serialize)` would only
167// emit the struct fields.
168#[cfg(feature = "serde")]
169impl Serialize for DistributionCompressedBinary {
170    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
171    where
172        S: serde::Serializer,
173    {
174        use serde::ser::SerializeStruct;
175        let mut s = serializer.serialize_struct("DistributionCompressedBinary", 6)?;
176        s.serialize_field("start_height", &self.start_height)?;
177        s.serialize_field("base", &self.base)?;
178        s.serialize_field(
179            "compressed_data",
180            &compress_integer_array(&self.distribution),
181        )?;
182        s.serialize_field("amount", &self.amount)?;
183        s.serialize_field("binary", &true)?;
184        s.serialize_field("compress", &true)?;
185        s.end()
186    }
187}
188
189#[cfg(feature = "epee")]
190epee_object! {
191    DistributionCompressedBinary,
192    start_height: u64,
193    base: u64,
194    distribution: Vec<u64>,
195    amount: u64,
196}
197
198/// Deserializer function for [`DistributionCompressedBinary::distribution`].
199///
200/// 1. Deserializes as `compressed_data` field.
201/// 2. Decompresses and returns the data
202#[cfg(feature = "serde")]
203fn deserialize_compressed_data_as_distribution<'de, D>(d: D) -> Result<Vec<u64>, D::Error>
204where
205    D: serde::Deserializer<'de>,
206{
207    Vec::<u8>::deserialize(d)
208        .and_then(|v| decompress_integer_array(&v).map_err(serde::de::Error::custom))
209}
210
211//---------------------------------------------------------------------------------------------------- Epee
212#[cfg(feature = "epee")]
213/// [`EpeeObjectBuilder`] for [`Distribution`].
214///
215/// Not for public usage.
216#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
217#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
218pub struct __DistributionEpeeBuilder {
219    pub start_height: Option<u64>,
220    pub base: Option<u64>,
221    pub distribution: Option<Vec<u64>>,
222    pub amount: Option<u64>,
223    pub compressed_data: Option<Vec<u8>>,
224    pub binary: Option<bool>,
225    pub compress: Option<bool>,
226}
227
228#[cfg(feature = "epee")]
229impl EpeeObjectBuilder<Distribution> for __DistributionEpeeBuilder {
230    fn add_field<B: Buf>(&mut self, name: &str, r: &mut B) -> error::Result<bool> {
231        match name {
232            "start_height" => self.start_height = Some(read_epee_value(r, Default::default())?),
233            "base" => self.base = Some(read_epee_value(r, Default::default())?),
234            "amount" => self.amount = Some(read_epee_value(r, Default::default())?),
235            "binary" => self.binary = Some(read_epee_value(r, Default::default())?),
236            "compress" => self.compress = Some(read_epee_value(r, Default::default())?),
237            "compressed_data" => {
238                self.compressed_data = Some(read_epee_value(r, Default::default())?);
239            }
240            // `distribution` arrives as a raw LE-u64 blob when `binary=true`
241            // (monerod uses `KV_SERIALIZE_CONTAINER_POD_AS_BLOB_N`) or as a
242            // typed EPEE u64 array when `binary=false`. Detect via marker.
243            "distribution" => {
244                let marker = read_marker(r)?;
245                self.distribution = Some(if marker == ContainerAsBlob::<u64>::MARKER {
246                    ContainerAsBlob::<u64>::read(r, &marker, Default::default())?.into()
247                } else {
248                    Vec::<u64>::read(r, &marker, Default::default())?
249                });
250            }
251            _ => return Ok(false),
252        }
253
254        Ok(true)
255    }
256
257    fn finish(self) -> error::Result<Distribution> {
258        const ELSE: error::Error = error::Error::Format("Required field was not found!");
259
260        let start_height = self.start_height.ok_or(ELSE)?;
261        let base = self.base.ok_or(ELSE)?;
262        let amount = self.amount.ok_or(ELSE)?;
263
264        let distribution = if let Some(compressed_data) = self.compressed_data {
265            let distribution = decompress_integer_array(&compressed_data)
266                .map_err(|_| error::Error::Format("Failed to decompress distribution"))?;
267            Distribution::CompressedBinary(DistributionCompressedBinary {
268                start_height,
269                base,
270                distribution,
271                amount,
272            })
273        } else {
274            let distribution = self.distribution.unwrap_or_default();
275            Distribution::Uncompressed(DistributionUncompressed {
276                binary: self.binary.unwrap_or(true),
277                distribution,
278                start_height,
279                base,
280                amount,
281            })
282        };
283
284        Ok(distribution)
285    }
286}
287
288#[cfg(feature = "epee")]
289impl EpeeObject for Distribution {
290    type Builder = __DistributionEpeeBuilder;
291
292    fn number_of_fields(&self) -> u64 {
293        match self {
294            // Inner struct fields + `compress`.
295            Self::Uncompressed(s) => s.number_of_fields() + 1,
296            // Inner struct fields + `compress` + `binary`.
297            Self::CompressedBinary(s) => s.number_of_fields() + 2,
298        }
299    }
300
301    fn write_fields<B: BufMut>(self, w: &mut B) -> error::Result<()> {
302        match self {
303            Self::Uncompressed(DistributionUncompressed {
304                start_height,
305                base,
306                distribution,
307                amount,
308                binary,
309            }) => {
310                write_field(start_height, "start_height", w)?;
311                write_field(base, "base", w)?;
312                if binary {
313                    write_field(ContainerAsBlob::from(distribution), "distribution", w)?;
314                } else {
315                    write_field(distribution, "distribution", w)?;
316                }
317                write_field(amount, "amount", w)?;
318                write_field(binary, "binary", w)?;
319                write_field(false, "compress", w)?;
320            }
321
322            Self::CompressedBinary(DistributionCompressedBinary {
323                start_height,
324                base,
325                distribution,
326                amount,
327            }) => {
328                let compressed_data = compress_integer_array(&distribution);
329
330                write_field(start_height, "start_height", w)?;
331                write_field(base, "base", w)?;
332                write_field(compressed_data, "compressed_data", w)?;
333                write_field(amount, "amount", w)?;
334                write_field(true, "binary", w)?;
335                write_field(true, "compress", w)?;
336            }
337        }
338
339        Ok(())
340    }
341}