Skip to main content

forest/lotus_json/
mod.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4//! In the Filecoin ecosystem, there are TWO different ways to present a domain object:
5//! - CBOR (defined in [`fvm_ipld_encoding`]).
6//!   This is the wire format.
7//! - JSON (see [`serde_json`]).
8//!   This is used in e.g RPC code, or in lotus printouts
9//!
10//! We care about compatibility with lotus/the Filecoin ecosystem for both.
11//! This module defines traits and types for handling both.
12//!
13//! # Terminology and background
14//! - A "domain object" is the _concept_ of an object.
15//!   E.g `"a CID with version = 1, codec = 0, and a multihash which is all zero"`
16//!   (This happens to be the default CID).
17//! - The "in memory" representation is how (rust) lays that out in memory.
18//!   See the definition of [`struct Cid { .. }`](`::cid::Cid`).
19//! - The "lotus JSON" is how [lotus](https://github.com/filecoin-project/lotus),
20//!   the reference Filecoin implementation, displays that object in JSON.
21//!   ```json
22//!   { "/": "baeaaaaa" }
23//!   ```
24//! - The "lotus CBOR" is how lotus represents that object on the wire.
25//!   ```rust
26//!   let in_memory = ::cid::Cid::default();
27//!   let cbor = fvm_ipld_encoding::to_vec(&in_memory).unwrap();
28//!   assert_eq!(
29//!       cbor,
30//!       0b_11011000_00101010_01000101_00000000_00000001_00000000_00000000_00000000_u64.to_be_bytes(),
31//!   );
32//!   ```
33//!
34//! In rust, the most common serialization framework is [`serde`].
35//! It has ONE (de)serialization model for each struct - the serialization code _cannot_ know
36//! if it's writing JSON or CBOR.
37//!
38//! The cleanest way handle the distinction would be a serde-compatible trait:
39//! ```rust
40//! # use serde::Serializer;
41//! pub trait LotusSerialize {
42//!     fn serialize_cbor<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
43//!     where
44//!         S: Serializer;
45//!
46//!     fn serialize_json<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
47//!     where
48//!         S: Serializer;
49//! }
50//! pub trait LotusDeserialize<'de> { /* ... */ }
51//! ```
52//!
53//! However, that would require writing and maintaining a custom derive macro - can we lean on
54//! [`macro@serde::Serialize`] and [`macro@serde::Deserialize`] instead?
55//!
56//! # Lotus JSON in Forest
57//! - Have a struct which represents a domain object: e.g [`GossipBlock`](crate::blocks::GossipBlock).
58//! - Implement [`serde::Serialize`] on that object, normally using [`fvm_ipld_encoding::tuple::Serialize_tuple`].
59//!   This corresponds to the CBOR representation.
60//! - Implement [`HasLotusJson`] on the domain object.
61//!   This attaches a separate JSON type, which should implement (`#[derive(...)]`) [`serde::Serialize`] and [`serde::Deserialize`] AND conversions to and from the domain object
62//!   E.g [`gossip_block`]
63//!
64//! Whenever you need the lotus JSON of an object, use the [`LotusJson`] wrapper.
65//! Note that the actual [`HasLotusJson::LotusJson`] types should be private - we don't want these names
66//! proliferating over the codebase.
67//!
68//! ## Implementation notes
69//! ### Illegal states are unrepresentable
70//! Consider [Address](crate::shim::address::Address) - it is represented as a simple string in JSON,
71//! so there are two possible definitions of `AddressLotusJson`:
72//! ```rust
73//! # use serde::{Deserialize, Serialize};
74//! # #[derive(Serialize, Deserialize)] enum Address {}
75//! # mod stringify {
76//! #     pub fn serialize<T, S: serde::Serializer>(_: &T, _: S) -> Result<S::Ok, S::Error> { unimplemented!() }
77//! #     pub fn deserialize<'de, T, D: serde::Deserializer<'de>>(_: D) -> Result<T, D::Error> { unimplemented!() }
78//! # }
79//! #[derive(Serialize, Deserialize)]
80//! pub struct AddressLotusJson(#[serde(with = "stringify")] Address);
81//! ```
82//! ```rust
83//! # use serde::{Deserialize, Serialize};
84//! #[derive(Serialize, Deserialize)]
85//! pub struct AddressLotusJson(String);
86//! ```
87//! However, with the second implementation, `impl From<AddressLotusJson> for Address` would involve unwrapping
88//! a call to [std::primitive::str::parse], which is unacceptable - malformed JSON could cause a crash!
89//!
90//! ### Location
91//! Prefer implementing in this module, as [`decl_and_test`] will handle `quickcheck`-ing and snapshot testing.
92//!
93//! If you require access to private fields, consider:
94//! - implementing an exhaustive helper method, e.g [`crate::beacon::BeaconEntry::into_parts`].
95//! - moving implementation to the module where the struct is defined, e.g [`crate::blocks::tipset::lotus_json`].
96//!   If you do this, you MUST manually add snapshot and `quickcheck` tests.
97//!
98//! ### Compound structs
99//! - Each field of a struct should be wrapped with [`LotusJson`].
100//! - Implementations of [`HasLotusJson::into_lotus_json`] and [`HasLotusJson::from_lotus_json`]
101//!   should use [`Into`] and [`LotusJson::into_inner`] calls
102//! - Use destructuring to ensure exhaustiveness
103//!
104//! ### Optional fields
105//! It's not clear if optional fields should be serialized as `null` or not.
106//! See e.g `LotusJson<Receipt>`.
107//!
108//! For now, fields are recommended to have the following annotations:
109//! ```rust,ignore
110//! # struct Foo {
111//! #[serde(skip_serializing_if = "LotusJson::is_none", default)]
112//! foo: LotusJson<Option<usize>>,
113//! # }
114//! ```
115//!
116//! # API hazards
117//! - Avoid using `#[serde(with = ...)]` except for leaf types
118//! - There is a hazard if the same type can be de/serialized in multiple ways.
119//!
120//! # Future work
121//! - use [`proptest`](https://docs.rs/proptest/) to test the parser pipeline
122//! - use a derive macro for simple compound structs
123
124use crate::shim::actors::miner::DeadlineInfo;
125use derive_more::From;
126use fvm_shared4::piece::PaddedPieceSize;
127#[cfg(test)]
128use pretty_assertions::assert_eq;
129use schemars::{JsonSchema, Schema, SchemaGenerator};
130use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
131#[cfg(test)]
132use serde_json::json;
133use std::{fmt::Display, str::FromStr};
134use uuid::Uuid;
135
136pub trait HasLotusJson: Sized {
137    /// The struct representing JSON. You should `#[derive(Deserialize, Serialize)]` on it.
138    type LotusJson: Serialize + DeserializeOwned;
139    /// To ensure code quality, conversion to/from lotus JSON MUST be tested.
140    /// Provide snapshots of the JSON, and the domain type it should serialize to.
141    ///
142    /// Serialization and de-serialization of the domain type should match the snapshot.
143    ///
144    /// If using [`decl_and_test`], this test is automatically run for you, but if the test
145    /// is out-of-module, you must call [`assert_all_snapshots`] manually.
146    #[cfg(test)]
147    fn snapshots() -> Vec<(serde_json::Value, Self)>;
148    fn into_lotus_json(self) -> Self::LotusJson;
149    fn from_lotus_json(lotus_json: Self::LotusJson) -> Self;
150    fn into_lotus_json_value(self) -> serde_json::Result<serde_json::Value> {
151        serde_json::to_value(self.into_lotus_json())
152    }
153    fn into_lotus_json_string(self) -> serde_json::Result<String> {
154        serde_json::to_string(&self.into_lotus_json())
155    }
156    fn into_lotus_json_string_pretty(self) -> serde_json::Result<String> {
157        serde_json::to_string_pretty(&self.into_lotus_json())
158    }
159}
160
161macro_rules! decl_and_test {
162    ($($mod_name:ident for $domain_ty:ty),* $(,)?) => {
163        $(
164            mod $mod_name;
165        )*
166        #[test]
167        fn all_snapshots() {
168            $(
169                print!("test snapshots for {}...", std::any::type_name::<$domain_ty>());
170                std::io::Write::flush(&mut std::io::stdout()).unwrap();
171                // ^ make sure the above line is flushed in case the test fails
172                assert_all_snapshots::<$domain_ty>();
173                println!("ok.");
174            )*
175        }
176        #[test]
177        fn all_quickchecks() {
178            $(
179                print!("quickcheck for {}...", std::any::type_name::<$domain_ty>());
180                std::io::Write::flush(&mut std::io::stdout()).unwrap();
181                // ^ make sure the above line is flushed in case the test fails
182                ::quickcheck::quickcheck(assert_unchanged_via_json::<$domain_ty> as fn(_));
183                println!("ok.");
184            )*
185        }
186    }
187}
188#[cfg(doc)]
189pub(crate) use decl_and_test;
190
191decl_and_test!(
192    actor_state for crate::shim::state_tree::ActorState,
193    address for crate::shim::address::Address,
194    beacon_entry for crate::beacon::BeaconEntry,
195    big_int for num::BigInt,
196    block_header for crate::blocks::CachingBlockHeader,
197    cid for ::cid::Cid,
198    duration for std::time::Duration,
199    percent for crate::shim::percent::Percent,
200    election_proof for crate::blocks::ElectionProof,
201    extended_sector_info for crate::shim::sector::ExtendedSectorInfo,
202    gossip_block for crate::blocks::GossipBlock,
203    key_info for crate::key_management::KeyInfo,
204    message for crate::shim::message::Message,
205    po_st_proof for crate::shim::sector::PoStProof,
206    registered_po_st_proof for crate::shim::sector::RegisteredPoStProof,
207    registered_seal_proof for crate::shim::sector::RegisteredSealProof,
208    sector_info for crate::shim::sector::SectorInfo,
209    sector_size for crate::shim::sector::SectorSize,
210    signature for crate::shim::crypto::Signature,
211    signature_type for crate::shim::crypto::SignatureType,
212    signed_message for  crate::message::SignedMessage,
213    ticket for crate::blocks::Ticket,
214    tipset_keys for crate::blocks::TipsetKey,
215    token_amount for crate::shim::econ::TokenAmount,
216    vec_u8 for Vec<u8>,
217    vrf_proof for crate::blocks::VRFProof,
218);
219
220// If a module cannot be tested normally above, you MAY declare it separately here
221// but you MUST document any tech debt - the reason WHY it cannot be tested above.
222mod actors;
223mod allocation;
224mod arc;
225mod beneficiary_term; // fil_actor_miner_state::v12::BeneficiaryTerm: !quickcheck::Arbitrary
226mod bit_field; //  fil_actors_shared::fvm_ipld_bitfield::BitField: !quickcheck::Arbitrary
227mod bytecode_hash;
228mod entry;
229mod filter_estimate;
230mod hash_map;
231mod ipld; // NaN != NaN
232mod miner_info; // fil_actor_miner_state::v12::MinerInfo: !quickcheck::Arbitrary
233mod miner_power; // actors::miner::MinerInfo: !quickcheck::Arbitrary
234mod nonempty; // can't make snapshots of generic type
235mod opt; // can't make snapshots of generic type
236mod padded_piece_size;
237mod pending_beneficiary_change; // fil_actor_miner_state::v12::PendingBeneficiaryChange: !quickcheck::Arbitrary
238mod power_claim; // actors::power::Claim: !quickcheck::Arbitrary
239mod raw_bytes; // fvm_ipld_encoding::RawBytes: !quickcheck::Arbitrary
240mod receipt; // shim type roundtrip is wrong - see module
241mod token_state;
242mod tombstone;
243mod transient_data;
244mod vec; // can't make snapshots of generic type
245mod verifreg_claim;
246
247pub use vec::*;
248
249#[macro_export]
250macro_rules! test_snapshots {
251    ($ty:ty) => {
252        pastey::paste! {
253            #[test]
254            fn [<snapshots_ $ty:snake>]() {
255                use super::*;
256                assert_all_snapshots::<$ty>();
257            }
258        }
259    };
260
261    ($module:path: $ty:ident: $($version:literal),+ $(,)?) => {
262        $(
263            pastey::paste! {
264                #[test]
265                fn [<snapshots_ $module _v $version _ $ty:lower>]() {
266                    use super::*;
267                    assert_all_snapshots::<$module::[<v $version>]::$ty>();
268                }
269            }
270        )+
271    };
272
273    ($module:path: $nested_path:path: $ty:ident: $($version:literal),+ $(,)?) => {
274        $(
275            pastey::paste! {
276                #[test]
277                fn [<snapshots_ $module _v $version _ $ty:lower>]() {
278                    use super::*;
279                    assert_all_snapshots::<$module::[<v $version>]::$nested_path::$ty>();
280                }
281            }
282        )+
283    };
284}
285
286#[cfg(any(test, doc))]
287pub fn assert_all_snapshots<T>()
288where
289    T: HasLotusJson,
290    <T as HasLotusJson>::LotusJson: PartialEq + std::fmt::Debug,
291{
292    let snapshots = T::snapshots();
293    assert!(!snapshots.is_empty());
294    for (lotus_json, val) in snapshots {
295        assert_one_snapshot(lotus_json, val);
296    }
297}
298
299#[cfg(test)]
300pub fn assert_one_snapshot<T>(lotus_json: serde_json::Value, val: T)
301where
302    T: HasLotusJson,
303    <T as HasLotusJson>::LotusJson: PartialEq + std::fmt::Debug,
304{
305    // T -> T::LotusJson -> lotus_json (Do not clone T as some external types do not implement Clone)
306    let val_lotus_json = val.into_lotus_json();
307    let serialized = serde_json::to_value(&val_lotus_json).unwrap();
308    assert_eq!(
309        serialized.to_string(),
310        lotus_json.to_string(),
311        "snapshot failed for {}",
312        std::any::type_name::<T>()
313    );
314
315    // lotus_json -> T::LotusJson -> T -> T::LotusJson
316    //( Not comparing T because external types may not implement `Eq` and `PartialEq`)
317    let deserialized = match serde_json::from_value::<T::LotusJson>(lotus_json.clone()) {
318        Ok(lotus_json) => T::from_lotus_json(lotus_json).into_lotus_json(),
319        Err(e) => panic!(
320            "couldn't deserialize a {} from {}: {e}",
321            std::any::type_name::<T::LotusJson>(),
322            lotus_json
323        ),
324    };
325    assert_eq!(deserialized, val_lotus_json);
326}
327
328#[cfg(any(test, doc))]
329pub fn assert_unchanged_via_json<T>(val: T)
330where
331    T: HasLotusJson + Clone + PartialEq + std::fmt::Debug,
332    T::LotusJson: Serialize + serde::de::DeserializeOwned,
333{
334    // T -> T::LotusJson -> lotus_json -> T::LotusJson -> T
335
336    // T -> T::LotusJson
337    let temp = val.clone().into_lotus_json();
338    // T::LotusJson -> lotus_json
339    let temp = serde_json::to_value(temp).unwrap();
340    // lotus_json -> T::LotusJson
341    let temp = serde_json::from_value::<T::LotusJson>(temp).unwrap();
342    // T::LotusJson -> T
343    let temp = T::from_lotus_json(temp);
344
345    assert_eq!(val, temp);
346}
347
348/// Usage: `#[serde(with = "stringify")]`
349pub mod stringify {
350    use super::*;
351
352    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
353    where
354        T: Display,
355        S: Serializer,
356    {
357        serializer.collect_str(value)
358    }
359
360    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
361    where
362        T: FromStr,
363        T::Err: Display,
364        D: Deserializer<'de>,
365    {
366        String::deserialize(deserializer)?
367            .parse()
368            .map_err(serde::de::Error::custom)
369    }
370}
371
372/// Usage: `#[serde(with = "hexify_bytes")]`
373pub mod hexify_bytes {
374    use super::*;
375
376    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
377    where
378        T: AsRef<[u8]>,
379        S: Serializer,
380    {
381        // full-width lower-case hex; `Display` of `ethereum_types` values would compress
382        // the middle, i.e. `0xff00…03ec`
383        serializer.serialize_str(&crate::utils::encoding::hex::encode_prefixed(value))
384    }
385
386    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
387    where
388        T: FromStr,
389        T::Err: Display,
390        D: Deserializer<'de>,
391    {
392        String::deserialize(deserializer)?
393            .parse()
394            .map_err(serde::de::Error::custom)
395    }
396}
397
398pub mod hexify_vec_bytes {
399    use super::*;
400    use std::borrow::Cow;
401
402    pub fn serialize<S>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error>
403    where
404        S: Serializer,
405    {
406        serializer.serialize_str(&crate::utils::encoding::hex::encode_prefixed(value))
407    }
408
409    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
410    where
411        D: Deserializer<'de>,
412    {
413        let s = String::deserialize(deserializer)?;
414        let s = Cow::from(s.strip_prefix("0x").unwrap_or(&s));
415
416        // Pad with 0 if odd length. This is necessary because decoding requires an even
417        // number of characters, whereas a valid input is also `0x0`.
418        let s = if s.len().is_multiple_of(2) {
419            s
420        } else {
421            let mut s = s.into_owned();
422            s.insert(0, '0');
423            Cow::Owned(s)
424        };
425
426        crate::utils::encoding::hex::decode(s.as_ref()).map_err(serde::de::Error::custom)
427    }
428}
429
430/// Usage: `#[serde(with = "hexify")]`
431pub mod hexify {
432    use super::*;
433    use num_traits::Num;
434    use serde::{Deserializer, Serializer};
435
436    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
437    where
438        T: Num + std::fmt::LowerHex,
439        S: Serializer,
440    {
441        serializer.serialize_str(format!("{value:#x}").as_str())
442    }
443
444    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
445    where
446        T: Num,
447        <T as Num>::FromStrRadixErr: std::fmt::Display,
448        D: Deserializer<'de>,
449    {
450        let s = String::deserialize(deserializer)?;
451        #[allow(clippy::indexing_slicing)]
452        if s.len() > 2 && &s[..2] == "0x" {
453            T::from_str_radix(&s[2..], 16).map_err(serde::de::Error::custom)
454        } else {
455            Err(serde::de::Error::custom("Invalid hex"))
456        }
457    }
458}
459
460/// Usage: `#[serde(with = "base64_standard")]`
461pub mod base64_standard {
462    use super::*;
463
464    use base64::engine::{Engine as _, general_purpose::STANDARD};
465
466    pub fn serialize<S>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error>
467    where
468        S: Serializer,
469    {
470        STANDARD.encode(value).serialize(serializer)
471    }
472
473    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
474    where
475        D: Deserializer<'de>,
476    {
477        STANDARD
478            .decode(String::deserialize(deserializer)?)
479            .map_err(serde::de::Error::custom)
480    }
481}
482
483/// MUST NOT be used in any `LotusJson` structs
484pub fn serialize<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
485where
486    S: Serializer,
487    T: HasLotusJson + Clone,
488{
489    value.clone().into_lotus_json().serialize(serializer)
490}
491
492/// MUST NOT be used in any `LotusJson` structs.
493pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
494where
495    D: Deserializer<'de>,
496    T: HasLotusJson,
497{
498    Ok(T::from_lotus_json(Deserialize::deserialize(deserializer)?))
499}
500
501/// A domain struct that is (de) serialized through its lotus JSON representation.
502#[derive(
503    Debug, Deserialize, From, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Clone,
504)]
505#[serde(bound = "T: HasLotusJson + Clone", transparent)]
506pub struct LotusJson<T>(#[serde(with = "self")] pub T);
507
508impl<T> JsonSchema for LotusJson<T>
509where
510    T: HasLotusJson,
511    T::LotusJson: JsonSchema,
512{
513    fn schema_name() -> std::borrow::Cow<'static, str> {
514        T::LotusJson::schema_name()
515    }
516
517    fn schema_id() -> std::borrow::Cow<'static, str> {
518        T::LotusJson::schema_id()
519    }
520
521    fn json_schema(g: &mut SchemaGenerator) -> Schema {
522        T::LotusJson::json_schema(g)
523    }
524}
525
526impl<T> LotusJson<T> {
527    pub fn into_inner(self) -> T {
528        self.0
529    }
530}
531
532macro_rules! lotus_json_with_self {
533    ($($domain_ty:ty),* $(,)?) => {
534        $(
535            impl $crate::lotus_json::HasLotusJson for $domain_ty {
536                type LotusJson = Self;
537                #[cfg(test)]
538                fn snapshots() -> Vec<(serde_json::Value, Self)> {
539                    unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
540                }
541                fn into_lotus_json(self) -> Self::LotusJson {
542                    self
543                }
544                fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
545                    lotus_json
546                }
547            }
548        )*
549    }
550}
551pub(crate) use lotus_json_with_self;
552
553lotus_json_with_self!(
554    u32,
555    u64,
556    i64,
557    f64,
558    String,
559    chrono::DateTime<chrono::Utc>,
560    serde_json::Value,
561    (),
562    std::path::PathBuf,
563    bool,
564    DeadlineInfo,
565    PaddedPieceSize,
566    Uuid,
567    std::num::NonZeroU32,
568    std::num::NonZeroUsize,
569);
570
571mod fixme {
572    use super::*;
573
574    impl<T: HasLotusJson> HasLotusJson for (T,) {
575        type LotusJson = (T::LotusJson,);
576        #[cfg(test)]
577        fn snapshots() -> Vec<(serde_json::Value, Self)> {
578            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
579        }
580        fn into_lotus_json(self) -> Self::LotusJson {
581            (self.0.into_lotus_json(),)
582        }
583        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
584            (HasLotusJson::from_lotus_json(lotus_json.0),)
585        }
586    }
587
588    impl<A: HasLotusJson, B: HasLotusJson> HasLotusJson for (A, B) {
589        type LotusJson = (A::LotusJson, B::LotusJson);
590        #[cfg(test)]
591        fn snapshots() -> Vec<(serde_json::Value, Self)> {
592            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
593        }
594        fn into_lotus_json(self) -> Self::LotusJson {
595            (self.0.into_lotus_json(), self.1.into_lotus_json())
596        }
597        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
598            (
599                HasLotusJson::from_lotus_json(lotus_json.0),
600                HasLotusJson::from_lotus_json(lotus_json.1),
601            )
602        }
603    }
604
605    impl<A: HasLotusJson, B: HasLotusJson, C: HasLotusJson> HasLotusJson for (A, B, C) {
606        type LotusJson = (A::LotusJson, B::LotusJson, C::LotusJson);
607        #[cfg(test)]
608        fn snapshots() -> Vec<(serde_json::Value, Self)> {
609            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
610        }
611        fn into_lotus_json(self) -> Self::LotusJson {
612            (
613                self.0.into_lotus_json(),
614                self.1.into_lotus_json(),
615                self.2.into_lotus_json(),
616            )
617        }
618        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
619            (
620                HasLotusJson::from_lotus_json(lotus_json.0),
621                HasLotusJson::from_lotus_json(lotus_json.1),
622                HasLotusJson::from_lotus_json(lotus_json.2),
623            )
624        }
625    }
626
627    impl<A: HasLotusJson, B: HasLotusJson, C: HasLotusJson, D: HasLotusJson> HasLotusJson
628        for (A, B, C, D)
629    {
630        type LotusJson = (A::LotusJson, B::LotusJson, C::LotusJson, D::LotusJson);
631        #[cfg(test)]
632        fn snapshots() -> Vec<(serde_json::Value, Self)> {
633            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
634        }
635        fn into_lotus_json(self) -> Self::LotusJson {
636            (
637                self.0.into_lotus_json(),
638                self.1.into_lotus_json(),
639                self.2.into_lotus_json(),
640                self.3.into_lotus_json(),
641            )
642        }
643        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
644            (
645                HasLotusJson::from_lotus_json(lotus_json.0),
646                HasLotusJson::from_lotus_json(lotus_json.1),
647                HasLotusJson::from_lotus_json(lotus_json.2),
648                HasLotusJson::from_lotus_json(lotus_json.3),
649            )
650        }
651    }
652}
653
654#[cfg(test)]
655mod tests {
656    use super::*;
657    use ipld_core::serde::SerdeError;
658    use quickcheck_macros::quickcheck;
659    use serde::de::{IntoDeserializer, value::StringDeserializer};
660
661    #[derive(Debug, Deserialize, Serialize, PartialEq)]
662    struct HexifyVecBytesTest {
663        #[serde(with = "hexify_vec_bytes")]
664        value: Vec<u8>,
665    }
666
667    /// [`hexify_bytes`] serialization matches the `format!("{value:#x}")` implementation
668    /// it replaced, for every `ethereum_types` value it serves.
669    fn matches_legacy_lowerhex<T: AsRef<[u8]> + std::fmt::LowerHex>(value: T) -> bool {
670        #[derive(Serialize)]
671        struct W<T: AsRef<[u8]>>(#[serde(with = "hexify_bytes")] T);
672
673        serde_json::to_string(&W(&value)).unwrap() == format!("\"{value:#x}\"")
674    }
675
676    fn filled<const N: usize>(bytes: Vec<u8>) -> [u8; N] {
677        let mut arr = [0; N];
678        arr.iter_mut().zip(bytes).for_each(|(a, b)| *a = b);
679        arr
680    }
681
682    #[test]
683    fn test_hexify_deserialize() {
684        fn de(input: &str) -> Result<u64, SerdeError> {
685            let deserializer: StringDeserializer<SerdeError> =
686                String::from_str(input).unwrap().into_deserializer();
687            hexify::deserialize(deserializer)
688        }
689
690        self::assert_eq!(de("0x2a").unwrap(), 42);
691        self::assert_eq!(de("0x0").unwrap(), 0);
692        for invalid in ["", "0x", "2a", "0xzz", "cthulhu"] {
693            assert!(de(invalid).is_err(), "{invalid:?} should be rejected");
694        }
695    }
696
697    #[quickcheck]
698    fn hexify_bytes_matches_legacy_h64(value: u64) -> bool {
699        matches_legacy_lowerhex(ethereum_types::H64::from_low_u64_be(value))
700    }
701
702    #[quickcheck]
703    fn hexify_bytes_matches_legacy_h160(bytes: Vec<u8>) -> bool {
704        matches_legacy_lowerhex(ethereum_types::H160::from(filled::<20>(bytes)))
705    }
706
707    #[quickcheck]
708    fn hexify_bytes_matches_legacy_bloom(bytes: Vec<u8>) -> bool {
709        matches_legacy_lowerhex(ethereum_types::Bloom::from(filled::<256>(bytes)))
710    }
711
712    #[test]
713    fn test_hexify_vec_bytes_serialize() {
714        let cases = [(vec![], "0x"), (vec![0], "0x00"), (vec![42, 66], "0x2a42")];
715
716        for (input, expected) in cases.into_iter() {
717            let hexify = HexifyVecBytesTest { value: input };
718            let serialized = serde_json::to_string(&hexify).unwrap();
719            self::assert_eq!(serialized, format!("{{\"value\":\"{}\"}}", expected));
720        }
721    }
722
723    #[test]
724    fn test_hexify_vec_bytes_deserialize() {
725        let cases = [
726            ("0x", vec![]),
727            ("0x0", vec![0]),
728            ("0xF", vec![15]),
729            ("0x2a42", vec![42, 66]),
730            ("0x2A42", vec![42, 66]),
731        ];
732
733        for (input, expected) in cases.into_iter() {
734            let deserializer: StringDeserializer<SerdeError> =
735                String::from_str(input).unwrap().into_deserializer();
736            let deserialized = hexify_vec_bytes::deserialize(deserializer).unwrap();
737            self::assert_eq!(deserialized, expected);
738        }
739
740        let fail_cases = ["cthulhu", "x", "0xazathoth"];
741        for input in fail_cases.into_iter() {
742            let deserializer: StringDeserializer<SerdeError> =
743                String::from_str(input).unwrap().into_deserializer();
744            let deserialized = hexify_vec_bytes::deserialize(deserializer);
745            assert!(deserialized.is_err());
746        }
747    }
748}