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        crate::utils::encoding::hex::parse_prefixed_int(&s).map_err(serde::de::Error::custom)
452    }
453}
454
455/// Usage: `#[serde(with = "base64_standard")]`
456pub mod base64_standard {
457    use super::*;
458
459    use base64::engine::{Engine as _, general_purpose::STANDARD};
460
461    pub fn serialize<S>(value: &[u8], serializer: S) -> Result<S::Ok, S::Error>
462    where
463        S: Serializer,
464    {
465        STANDARD.encode(value).serialize(serializer)
466    }
467
468    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
469    where
470        D: Deserializer<'de>,
471    {
472        STANDARD
473            .decode(String::deserialize(deserializer)?)
474            .map_err(serde::de::Error::custom)
475    }
476}
477
478/// MUST NOT be used in any `LotusJson` structs
479pub fn serialize<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
480where
481    S: Serializer,
482    T: HasLotusJson + Clone,
483{
484    value.clone().into_lotus_json().serialize(serializer)
485}
486
487/// MUST NOT be used in any `LotusJson` structs.
488pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
489where
490    D: Deserializer<'de>,
491    T: HasLotusJson,
492{
493    Ok(T::from_lotus_json(Deserialize::deserialize(deserializer)?))
494}
495
496/// A domain struct that is (de) serialized through its lotus JSON representation.
497#[derive(
498    Debug, Deserialize, From, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Clone,
499)]
500#[serde(bound = "T: HasLotusJson + Clone", transparent)]
501pub struct LotusJson<T>(#[serde(with = "self")] pub T);
502
503impl<T> JsonSchema for LotusJson<T>
504where
505    T: HasLotusJson,
506    T::LotusJson: JsonSchema,
507{
508    fn schema_name() -> std::borrow::Cow<'static, str> {
509        T::LotusJson::schema_name()
510    }
511
512    fn schema_id() -> std::borrow::Cow<'static, str> {
513        T::LotusJson::schema_id()
514    }
515
516    fn json_schema(g: &mut SchemaGenerator) -> Schema {
517        T::LotusJson::json_schema(g)
518    }
519}
520
521impl<T> LotusJson<T> {
522    pub fn into_inner(self) -> T {
523        self.0
524    }
525}
526
527macro_rules! lotus_json_with_self {
528    ($($domain_ty:ty),* $(,)?) => {
529        $(
530            impl $crate::lotus_json::HasLotusJson for $domain_ty {
531                type LotusJson = Self;
532                #[cfg(test)]
533                fn snapshots() -> Vec<(serde_json::Value, Self)> {
534                    unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
535                }
536                fn into_lotus_json(self) -> Self::LotusJson {
537                    self
538                }
539                fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
540                    lotus_json
541                }
542            }
543        )*
544    }
545}
546pub(crate) use lotus_json_with_self;
547
548lotus_json_with_self!(
549    u32,
550    u64,
551    i64,
552    f64,
553    String,
554    chrono::DateTime<chrono::Utc>,
555    serde_json::Value,
556    (),
557    std::path::PathBuf,
558    bool,
559    DeadlineInfo,
560    PaddedPieceSize,
561    Uuid,
562    std::num::NonZeroU32,
563    std::num::NonZeroUsize,
564);
565
566mod fixme {
567    use super::*;
568
569    impl<T: HasLotusJson> HasLotusJson for (T,) {
570        type LotusJson = (T::LotusJson,);
571        #[cfg(test)]
572        fn snapshots() -> Vec<(serde_json::Value, Self)> {
573            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
574        }
575        fn into_lotus_json(self) -> Self::LotusJson {
576            (self.0.into_lotus_json(),)
577        }
578        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
579            (HasLotusJson::from_lotus_json(lotus_json.0),)
580        }
581    }
582
583    impl<A: HasLotusJson, B: HasLotusJson> HasLotusJson for (A, B) {
584        type LotusJson = (A::LotusJson, B::LotusJson);
585        #[cfg(test)]
586        fn snapshots() -> Vec<(serde_json::Value, Self)> {
587            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
588        }
589        fn into_lotus_json(self) -> Self::LotusJson {
590            (self.0.into_lotus_json(), self.1.into_lotus_json())
591        }
592        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
593            (
594                HasLotusJson::from_lotus_json(lotus_json.0),
595                HasLotusJson::from_lotus_json(lotus_json.1),
596            )
597        }
598    }
599
600    impl<A: HasLotusJson, B: HasLotusJson, C: HasLotusJson> HasLotusJson for (A, B, C) {
601        type LotusJson = (A::LotusJson, B::LotusJson, C::LotusJson);
602        #[cfg(test)]
603        fn snapshots() -> Vec<(serde_json::Value, Self)> {
604            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
605        }
606        fn into_lotus_json(self) -> Self::LotusJson {
607            (
608                self.0.into_lotus_json(),
609                self.1.into_lotus_json(),
610                self.2.into_lotus_json(),
611            )
612        }
613        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
614            (
615                HasLotusJson::from_lotus_json(lotus_json.0),
616                HasLotusJson::from_lotus_json(lotus_json.1),
617                HasLotusJson::from_lotus_json(lotus_json.2),
618            )
619        }
620    }
621
622    impl<A: HasLotusJson, B: HasLotusJson, C: HasLotusJson, D: HasLotusJson> HasLotusJson
623        for (A, B, C, D)
624    {
625        type LotusJson = (A::LotusJson, B::LotusJson, C::LotusJson, D::LotusJson);
626        #[cfg(test)]
627        fn snapshots() -> Vec<(serde_json::Value, Self)> {
628            unimplemented!("tests are trivial for HasLotusJson<LotusJson = Self>")
629        }
630        fn into_lotus_json(self) -> Self::LotusJson {
631            (
632                self.0.into_lotus_json(),
633                self.1.into_lotus_json(),
634                self.2.into_lotus_json(),
635                self.3.into_lotus_json(),
636            )
637        }
638        fn from_lotus_json(lotus_json: Self::LotusJson) -> Self {
639            (
640                HasLotusJson::from_lotus_json(lotus_json.0),
641                HasLotusJson::from_lotus_json(lotus_json.1),
642                HasLotusJson::from_lotus_json(lotus_json.2),
643                HasLotusJson::from_lotus_json(lotus_json.3),
644            )
645        }
646    }
647}
648
649#[cfg(test)]
650mod tests {
651    use super::*;
652    use ipld_core::serde::SerdeError;
653    use quickcheck_macros::quickcheck;
654    use serde::de::{IntoDeserializer, value::StringDeserializer};
655
656    #[derive(Debug, Deserialize, Serialize, PartialEq)]
657    struct HexifyVecBytesTest {
658        #[serde(with = "hexify_vec_bytes")]
659        value: Vec<u8>,
660    }
661
662    /// [`hexify_bytes`] serialization matches the `format!("{value:#x}")` implementation
663    /// it replaced, for every `ethereum_types` value it serves.
664    fn matches_legacy_lowerhex<T: AsRef<[u8]> + std::fmt::LowerHex>(value: T) -> bool {
665        #[derive(Serialize)]
666        struct W<T: AsRef<[u8]>>(#[serde(with = "hexify_bytes")] T);
667
668        serde_json::to_string(&W(&value)).unwrap() == format!("\"{value:#x}\"")
669    }
670
671    fn filled<const N: usize>(bytes: Vec<u8>) -> [u8; N] {
672        let mut arr = [0; N];
673        arr.iter_mut().zip(bytes).for_each(|(a, b)| *a = b);
674        arr
675    }
676
677    #[test]
678    fn test_hexify_deserialize() {
679        fn de(input: &str) -> Result<u64, SerdeError> {
680            let deserializer: StringDeserializer<SerdeError> =
681                String::from_str(input).unwrap().into_deserializer();
682            hexify::deserialize(deserializer)
683        }
684
685        self::assert_eq!(de("0x2a").unwrap(), 42);
686        self::assert_eq!(de("0x0").unwrap(), 0);
687        // "0é" is 3 bytes, so slicing a byte-length-checked prefix would panic here.
688        for invalid in ["", "0x", "2a", "0xzz", "cthulhu", "0é", "0x-1"] {
689            assert!(de(invalid).is_err(), "{invalid:?} should be rejected");
690        }
691    }
692
693    #[quickcheck]
694    fn hexify_bytes_matches_legacy_h64(value: u64) -> bool {
695        matches_legacy_lowerhex(ethereum_types::H64::from_low_u64_be(value))
696    }
697
698    #[quickcheck]
699    fn hexify_bytes_matches_legacy_h160(bytes: Vec<u8>) -> bool {
700        matches_legacy_lowerhex(ethereum_types::H160::from(filled::<20>(bytes)))
701    }
702
703    #[quickcheck]
704    fn hexify_bytes_matches_legacy_bloom(bytes: Vec<u8>) -> bool {
705        matches_legacy_lowerhex(ethereum_types::Bloom::from(filled::<256>(bytes)))
706    }
707
708    #[test]
709    fn test_hexify_vec_bytes_serialize() {
710        let cases = [(vec![], "0x"), (vec![0], "0x00"), (vec![42, 66], "0x2a42")];
711
712        for (input, expected) in cases.into_iter() {
713            let hexify = HexifyVecBytesTest { value: input };
714            let serialized = serde_json::to_string(&hexify).unwrap();
715            self::assert_eq!(serialized, format!("{{\"value\":\"{}\"}}", expected));
716        }
717    }
718
719    #[test]
720    fn test_hexify_vec_bytes_deserialize() {
721        let cases = [
722            ("0x", vec![]),
723            ("0x0", vec![0]),
724            ("0xF", vec![15]),
725            ("0x2a42", vec![42, 66]),
726            ("0x2A42", vec![42, 66]),
727        ];
728
729        for (input, expected) in cases.into_iter() {
730            let deserializer: StringDeserializer<SerdeError> =
731                String::from_str(input).unwrap().into_deserializer();
732            let deserialized = hexify_vec_bytes::deserialize(deserializer).unwrap();
733            self::assert_eq!(deserialized, expected);
734        }
735
736        let fail_cases = ["cthulhu", "x", "0xazathoth"];
737        for input in fail_cases.into_iter() {
738            let deserializer: StringDeserializer<SerdeError> =
739                String::from_str(input).unwrap().into_deserializer();
740            let deserialized = hexify_vec_bytes::deserialize(deserializer);
741            assert!(deserialized.is_err());
742        }
743    }
744}