Skip to main content

dpp_json_convertible_derive/
lib.rs

1extern crate proc_macro;
2
3use proc_macro::TokenStream;
4use proc_macro2::Span;
5use quote::{quote, ToTokens};
6use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Item, Type};
7
8const DEFAULT_BASE_PATH: &str = "crate::serialization";
9
10/// Attribute macro that auto-injects `#[serde(with = "...")]` on fields whose
11/// natural serde shape would round-trip badly through JSON / WASM.
12///
13/// Works on both **structs** and **enums** (with named fields in variants).
14///
15/// # What it injects
16///
17/// | Field type | Helper injected | Wire shape |
18/// |---|---|---|
19/// | `u64` / `Option<u64>` and known aliases (`Credits`, `TokenAmount`, `TimestampMillis`, `BlockHeight`, …) | `json_safe_u64` / `json_safe_option_u64` | number ≤ `MAX_SAFE_INTEGER`, otherwise string — avoids JS precision loss |
20/// | `i64` / `Option<i64>` and known aliases | `json_safe_i64` / `json_safe_option_i64` | same, signed |
21/// | `[u8; N]` (any `N`, via const generics) | `serde_bytes` | raw bytes in binary, base64 string in JSON |
22/// | `Vec<u8>` | `serde_bytes_var` | raw bytes in binary, base64 string in JSON |
23///
24/// The byte-field handling matches the broader codebase convention used by
25/// `Bytes20` / `Bytes32` / `Bytes36` / `BinaryData` in `rs-platform-value`.
26///
27/// Fields that already carry an explicit `#[serde(with = "…")]` (or
28/// `skip` / `flatten` / `cfg_attr` wrapping a `serde(with)`) are left
29/// untouched.
30///
31/// # Compile-time transitive guarantee
32///
33/// When applied inside the `dpp` crate, the macro also implements the marker
34/// trait `JsonSafeFields` for the type, and emits compile-time assertions that
35/// every nested non-primitive field type (anything not directly handled above)
36/// also implements `JsonSafeFields`. This propagates the safety check
37/// transitively — adding a new struct that contains a `Credits` without an
38/// `#[json_safe_fields]` annotation will fail to compile.
39///
40/// # Feature flags
41///
42/// When serde derives are behind `cfg_attr(feature = "...")`, the `cfg_attr` is
43/// evaluated by the compiler BEFORE this macro runs. If the feature is off,
44/// serde derives aren't visible and `#[serde(with)]` is NOT generated — which
45/// is correct because `serde(with)` requires an active serde derive.
46///
47/// # Inside `dpp` crate (default)
48/// ```ignore
49/// #[json_safe_fields]
50/// pub struct MyStructV0 {
51///     pub supply: u64,          // → auto-annotated with crate::serialization::json_safe_u64
52///     pub anchor: [u8; 32],     // → auto-annotated with crate::serialization::serde_bytes
53///     pub proof: Vec<u8>,       // → auto-annotated with crate::serialization::serde_bytes_var
54///     pub name: String,         // → untouched
55/// }
56/// ```
57///
58/// # Outside `dpp` crate
59/// Use `crate = "..."` to specify the path to the dpp serialization module.
60/// When `crate` is specified, the `JsonSafeFields` impl is NOT generated.
61/// ```ignore
62/// #[json_safe_fields(crate = "dash_sdk::dpp")]
63/// pub struct MyWasmStruct {
64///     pub balance: u64,         // → annotated with dash_sdk::dpp::serialization::json_safe_u64
65/// }
66/// ```
67#[proc_macro_attribute]
68pub fn json_safe_fields(attr: TokenStream, item: TokenStream) -> TokenStream {
69    // Parse optional `crate = "..."` attribute
70    let (base_path, is_external) = if attr.is_empty() {
71        (DEFAULT_BASE_PATH.to_string(), false)
72    } else {
73        match syn::parse::<syn::MetaNameValue>(attr) {
74            Ok(meta) if meta.path.is_ident("crate") => {
75                if let syn::Expr::Lit(syn::ExprLit {
76                    lit: syn::Lit::Str(lit_str),
77                    ..
78                }) = &meta.value
79                {
80                    (format!("{}::serialization", lit_str.value()), true)
81                } else {
82                    return syn::Error::new_spanned(meta.value, "expected string literal")
83                        .to_compile_error()
84                        .into();
85                }
86            }
87            Ok(meta) => {
88                return syn::Error::new_spanned(meta.path, "expected `crate = \"...\"`")
89                    .to_compile_error()
90                    .into();
91            }
92            Err(e) => return e.to_compile_error().into(),
93        }
94    };
95
96    let input = syn::parse::<Item>(item);
97    let item = match input {
98        Ok(item) => item,
99        Err(e) => return e.to_compile_error().into(),
100    };
101
102    match item {
103        Item::Struct(mut s) => {
104            // Check if serde derives are present.
105            // NOTE: cfg_attr is evaluated by the compiler BEFORE attribute macros run.
106            // If serde derives are behind cfg_attr with a disabled feature, we won't
107            // see them — which is correct (no serde(with) without an active derive).
108            let mut nested_types = Vec::new();
109            if has_serde_derive(&s.attrs) {
110                if let Fields::Named(ref mut fields) = s.fields {
111                    nested_types = annotate_fields(fields, &base_path);
112                }
113            }
114
115            // Only generate JsonSafeFields impl when used inside dpp (no crate override)
116            if is_external {
117                quote! { #s }.into()
118            } else {
119                let name = &s.ident;
120                let generics = &s.generics;
121                let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
122
123                let expanded = if nested_types.is_empty() {
124                    quote! {
125                        #s
126
127                        impl #impl_generics crate::serialization::JsonSafeFields for #name #ty_generics #where_clause {}
128                    }
129                } else {
130                    let assertions = generate_nested_assertions(&nested_types);
131                    quote! {
132                        #s
133
134                        impl #impl_generics crate::serialization::JsonSafeFields for #name #ty_generics #where_clause {}
135
136                        #[cfg(feature = "json-conversion")]
137                        #assertions
138                    }
139                };
140                expanded.into()
141            }
142        }
143        Item::Enum(mut e) => {
144            let mut nested_types = Vec::new();
145            if has_serde_derive(&e.attrs) {
146                for variant in e.variants.iter_mut() {
147                    if let Fields::Named(ref mut fields) = variant.fields {
148                        nested_types.extend(annotate_fields(fields, &base_path));
149                    }
150                }
151            }
152
153            if is_external {
154                quote! { #e }.into()
155            } else {
156                let name = &e.ident;
157                let generics = &e.generics;
158                let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
159
160                let expanded = if nested_types.is_empty() {
161                    quote! {
162                        #e
163
164                        impl #impl_generics crate::serialization::JsonSafeFields for #name #ty_generics #where_clause {}
165                    }
166                } else {
167                    let assertions = generate_nested_assertions(&nested_types);
168                    quote! {
169                        #e
170
171                        impl #impl_generics crate::serialization::JsonSafeFields for #name #ty_generics #where_clause {}
172
173                        #[cfg(feature = "json-conversion")]
174                        #assertions
175                    }
176                };
177                expanded.into()
178            }
179        }
180        other => {
181            let err = syn::Error::new_spanned(
182                quote! { #other },
183                "#[json_safe_fields] can only be applied to structs or enums",
184            );
185            err.to_compile_error().into()
186        }
187    }
188}
189
190/// Generate compile-time assertions that all nested field types implement `JsonSafeFields`.
191///
192/// This ensures that if a struct contains a field like `config: DistributionFunction`,
193/// the compiler will verify that `DistributionFunction` also has `#[json_safe_fields]`.
194fn generate_nested_assertions(types: &[Type]) -> proc_macro2::TokenStream {
195    if types.is_empty() {
196        return quote! {};
197    }
198
199    // Deduplicate types by their string representation
200    let mut seen = std::collections::HashSet::new();
201    let mut assertions = Vec::new();
202
203    for (i, ty) in types.iter().enumerate() {
204        let type_str = quote! { #ty }.to_string();
205        if seen.insert(type_str) {
206            let fn_name = Ident::new(
207                &format!("_assert_json_safe_fields_{}", i),
208                Span::call_site(),
209            );
210            assertions.push(quote! {
211                fn #fn_name<T: crate::serialization::JsonSafeFields>() {}
212                let _ = #fn_name::<#ty>;
213            });
214        }
215    }
216
217    quote! {
218        const _: () = {
219            #(#assertions)*
220        };
221    }
222}
223
224/// Add `#[serde(with = "...")]` to u64/i64 fields in a named fields block.
225/// Skips fields that already have `#[serde(with)]`.
226///
227/// Returns the types of fields that were NOT annotated (i.e., fields that are not
228/// u64/i64 and don't already have `serde(with)`). These types should be checked
229/// for `JsonSafeFields` via compile-time assertions.
230fn annotate_fields(fields: &mut syn::FieldsNamed, base_path: &str) -> Vec<Type> {
231    let mut unannotated_types = Vec::new();
232    for field in fields.named.iter_mut() {
233        if let Some(suffix) = serde_with_suffix_for_type(&field.ty) {
234            let with_path = format!("{}::{}", base_path, suffix);
235            let already_has_serde_with = field.attrs.iter().any(|attr| {
236                if attr.path().is_ident("serde") {
237                    let mut found = false;
238                    let _ = attr.parse_nested_meta(|meta| {
239                        if meta.path.is_ident("with") {
240                            found = true;
241                        }
242                        Ok(())
243                    });
244                    found
245                } else {
246                    false
247                }
248            });
249            if !already_has_serde_with {
250                // For Option types, also add `default` so missing fields
251                // deserialize as None. Without this, serde(with) overrides serde's
252                // built-in Option handling and missing fields cause errors.
253                if suffix.contains("option") {
254                    let already_has_serde_default = has_serde_default(&field.attrs);
255                    if already_has_serde_default {
256                        // Already has default, just add with
257                        field.attrs.push(syn::parse_quote! {
258                            #[serde(with = #with_path)]
259                        });
260                    } else {
261                        // Combine default + with in a single attribute to avoid duplicates
262                        field.attrs.push(syn::parse_quote! {
263                            #[serde(default, with = #with_path)]
264                        });
265                    }
266                } else {
267                    field.attrs.push(syn::parse_quote! {
268                        #[serde(with = #with_path)]
269                    });
270                }
271            }
272        } else {
273            // This field is not u64/i64 — its type must implement JsonSafeFields
274            // to guarantee it doesn't contain unprotected large integers.
275            // Skip fields that have explicit serde handling:
276            // - skip/skip_serializing/skip_deserializing: not serialized
277            // - flatten: special serde handling
278            // - with: developer explicitly controls serialization
279            let has_serde_override = field.attrs.iter().any(|attr| {
280                if attr.path().is_ident("serde") {
281                    let mut found = false;
282                    let _ = attr.parse_nested_meta(|meta| {
283                        if meta.path.is_ident("skip")
284                            || meta.path.is_ident("skip_serializing")
285                            || meta.path.is_ident("skip_deserializing")
286                            || meta.path.is_ident("flatten")
287                            || meta.path.is_ident("with")
288                        {
289                            found = true;
290                        }
291                        Ok(())
292                    });
293                    found
294                } else {
295                    false
296                }
297            });
298            // Also check for serde(with) inside cfg_attr, e.g.
299            // #[cfg_attr(feature = "json-conversion", serde(with = "..."))]
300            let has_cfg_attr_serde_with = field.attrs.iter().any(|attr| {
301                if attr.path().is_ident("cfg_attr") {
302                    let tokens = attr.meta.to_token_stream().to_string();
303                    tokens.contains("serde") && tokens.contains("with")
304                } else {
305                    false
306                }
307            });
308            if !has_serde_override && !has_cfg_attr_serde_with {
309                unannotated_types.push(field.ty.clone());
310            }
311        }
312    }
313    unannotated_types
314}
315
316/// Determine if a type needs a serde `with` annotation and return the module name suffix.
317///
318/// Matches:
319/// - `u64`, `i64`, `Option<u64>`, `Option<i64>`, and known u64/i64 type aliases
320///   (e.g. `Credits`, `TokenAmount`, `TimestampMillis`) → `json_safe_*` helpers
321/// - `[u8; N]` for any `N` → `serde_bytes` (const-generic; raw bytes in binary,
322///   base64 string in JSON)
323/// - `Vec<u8>` → `serde_bytes_var` (variable-length variant of the above)
324///
325/// When adding a new `type X = u64` alias in rs-dpp, add it to the appropriate list below.
326fn serde_with_suffix_for_type(ty: &Type) -> Option<&'static str> {
327    match ty {
328        // [u8; N] — any fixed-size byte array, length-agnostic via const generics.
329        Type::Array(arr) if is_u8_type(&arr.elem) => Some("serde_bytes"),
330        Type::Path(type_path) => {
331            let segments = &type_path.path.segments;
332            // Get the last segment — handles both `u64` and `std::u64` / `crate::prelude::Credits`
333            let last = segments.last()?;
334            let ident = &last.ident;
335
336            if is_u64_type(ident) {
337                return Some("json_safe_u64");
338            }
339            if is_i64_type(ident) {
340                return Some("json_safe_i64");
341            }
342            // Vec<u8> → variable-length bytes helper (base64 in JSON, raw in binary).
343            if ident == "Vec" {
344                if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
345                    if args.args.len() == 1 {
346                        if let syn::GenericArgument::Type(inner_ty) = &args.args[0] {
347                            if is_u8_type(inner_ty) {
348                                return Some("serde_bytes_var");
349                            }
350                        }
351                    }
352                }
353            }
354            // Check for Option<u64/i64/alias> — handles both `Option<T>` and `std::option::Option<T>`
355            if ident == "Option" {
356                if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
357                    if args.args.len() == 1 {
358                        if let syn::GenericArgument::Type(Type::Path(inner)) = &args.args[0] {
359                            if let Some(inner_last) = inner.path.segments.last() {
360                                if is_u64_type(&inner_last.ident) {
361                                    return Some("json_safe_option_u64");
362                                }
363                                if is_i64_type(&inner_last.ident) {
364                                    return Some("json_safe_option_i64");
365                                }
366                            }
367                        }
368                    }
369                }
370            }
371            None
372        }
373        _ => None,
374    }
375}
376
377/// Known type aliases that resolve to u64.
378/// Keep in sync with type aliases defined in rs-dpp.
379const U64_ALIASES: &[&str] = &[
380    "BlockHeight",
381    "BlockHeightInterval",
382    "Credits",
383    "Duffs",
384    "FeeMultiplier",
385    "IdentityNonce",
386    "ProtocolVersionVoteCount",
387    "RemainingCredits",
388    "Revision",
389    "TimestampMillis",
390    "TimestampMillisInterval",
391    "TokenAmount",
392    "TokenDistributionWeight",
393    "WithdrawalTransactionIndex",
394];
395
396/// Known type aliases that resolve to i64.
397const I64_ALIASES: &[&str] = &["SignedCredits", "SignedTokenAmount"];
398
399/// Check if the struct has serde derives (Serialize or Deserialize) in its attributes.
400///
401/// NOTE: `cfg_attr` is evaluated by the compiler BEFORE attribute macros run.
402/// If serde derives are behind `cfg_attr(feature = "...", derive(Serialize))` and the
403/// feature is disabled, the `cfg_attr` is already stripped — so we won't see it, which
404/// is the correct behavior (we shouldn't add `#[serde(with)]` when serde isn't derived).
405fn has_serde_derive(attrs: &[syn::Attribute]) -> bool {
406    for attr in attrs {
407        if attr.path().is_ident("derive") {
408            if let Ok(list) = attr.meta.require_list() {
409                // Parse the derive list to find exact Serialize/Deserialize idents
410                // Must handle both `Serialize` and `serde::Serialize` forms
411                let tokens = list.tokens.clone();
412                for tt in tokens.into_iter() {
413                    if let proc_macro2::TokenTree::Ident(ident) = &tt {
414                        let name = ident.to_string();
415                        // Exact ident match — "PlatformSerialize" is a single ident,
416                        // not "Platform" + "Serialize", so this won't false-match.
417                        if name == "Serialize" || name == "Deserialize" {
418                            return true;
419                        }
420                    }
421                }
422            }
423        }
424    }
425    false
426}
427
428/// Check if a field already has `#[serde(default)]` or `#[serde(default = "...")]`
429/// in any of its attributes, including inside `#[cfg_attr(..., serde(..., default))]`.
430///
431/// Note: cfg_attr on fields inside a struct body is NOT evaluated before
432/// the outer attribute macro processes the struct, so we must scan tokens manually.
433fn has_serde_default(attrs: &[syn::Attribute]) -> bool {
434    for attr in attrs {
435        let full_tokens = quote! { #attr }.to_string();
436        // Check if this attribute contains "serde" and a "default" token
437        if full_tokens.contains("serde") && full_tokens.contains("default") {
438            if let Ok(list) = attr.meta.require_list() {
439                let token_str = list.tokens.to_string();
440                // Match "default" as standalone or "default = ..." (custom fn)
441                if has_default_in_token_str(&token_str) {
442                    return true;
443                }
444                // Also check inside nested groups (cfg_attr case)
445                // e.g., cfg_attr(feature = "...", serde(rename = "...", default))
446                for tt in list.tokens.clone() {
447                    if let proc_macro2::TokenTree::Group(group) = tt {
448                        let inner = group.stream().to_string();
449                        if has_default_in_token_str(&inner) {
450                            return true;
451                        }
452                    }
453                }
454            }
455        }
456    }
457    false
458}
459
460/// Check if a comma-separated token string contains a "default" item,
461/// either standalone (`default`) or with a custom function (`default = "..."`).
462fn has_default_in_token_str(s: &str) -> bool {
463    s.split(',').any(|part| {
464        let trimmed = part.trim();
465        trimmed == "default" || trimmed.starts_with("default =") || trimmed.starts_with("default=")
466    })
467}
468
469fn is_u64_type(ident: &Ident) -> bool {
470    ident == "u64" || U64_ALIASES.iter().any(|alias| ident == alias)
471}
472
473fn is_i64_type(ident: &Ident) -> bool {
474    ident == "i64" || I64_ALIASES.iter().any(|alias| ident == alias)
475}
476
477/// True when the type resolves to `u8` — used to detect `[u8; N]` and `Vec<u8>`
478/// byte fields and auto-inject the matching `serde_bytes_*` helper.
479fn is_u8_type(ty: &Type) -> bool {
480    if let Type::Path(type_path) = ty {
481        if let Some(last) = type_path.path.segments.last() {
482            return last.ident == "u8" && last.arguments.is_empty();
483        }
484    }
485    false
486}
487
488/// Derive macro that generates `impl JsonConvertible for Type {}` with
489/// compile-time assertions that all inner types implement `JsonSafeFields`.
490///
491/// Feature gates are handled externally via `cfg_attr`:
492/// ```ignore
493/// #[cfg_attr(feature = "json-conversion", derive(JsonConvertible))]
494/// #[serde(tag = "$formatVersion")]
495/// pub enum TokenConfiguration {
496///     #[serde(rename = "0")]
497///     V0(TokenConfigurationV0),
498/// }
499/// ```
500#[proc_macro_derive(JsonConvertible)]
501pub fn derive_json_convertible(input: TokenStream) -> TokenStream {
502    let input = parse_macro_input!(input as DeriveInput);
503    let name = &input.ident;
504    let generics = &input.generics;
505    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
506
507    // Collect inner types from enum variants for compile-time assertions
508    let assertions = match &input.data {
509        Data::Enum(data_enum) => {
510            let mut assertions = Vec::new();
511            for variant in &data_enum.variants {
512                if let Fields::Unnamed(fields) = &variant.fields {
513                    for (i, field) in fields.unnamed.iter().enumerate() {
514                        let ty = &field.ty;
515                        let assert_fn_name = Ident::new(
516                            &format!(
517                                "must_have_json_safe_fields_attribute_{}_{}",
518                                variant.ident.to_string().to_lowercase(),
519                                i
520                            ),
521                            Span::call_site(),
522                        );
523                        assertions.push(quote! {
524                            fn #assert_fn_name<T: crate::serialization::JsonSafeFields>() {}
525                            let _ = #assert_fn_name::<#ty>;
526                        });
527                    }
528                }
529            }
530            assertions
531        }
532        Data::Struct(_) => {
533            // For plain structs, assert that Self implements JsonSafeFields
534            vec![quote! {
535                fn must_have_json_safe_fields_attribute<T: crate::serialization::JsonSafeFields>() {}
536                let _ = must_have_json_safe_fields_attribute::<#name #ty_generics>;
537            }]
538        }
539        Data::Union(_) => {
540            return syn::Error::new_spanned(name, "JsonConvertible cannot be derived for unions")
541                .to_compile_error()
542                .into();
543        }
544    };
545
546    let json_safe_fields_impl = match &input.data {
547        Data::Enum(_) => {
548            // Enums get JsonSafeFields — their inner V0 types are verified below.
549            quote! {
550                impl #impl_generics crate::serialization::JsonSafeFields for #name #ty_generics #where_clause {}
551            }
552        }
553        _ => {
554            // Structs already have JsonSafeFields from #[json_safe_fields] — don't duplicate.
555            quote! {}
556        }
557    };
558
559    let expanded = quote! {
560        impl #impl_generics crate::serialization::JsonConvertible for #name #ty_generics #where_clause {}
561
562        #json_safe_fields_impl
563
564        const _: () = {
565            #(#assertions)*
566        };
567    };
568
569    expanded.into()
570}
571
572/// Derive macro that generates `impl ValueConvertible for Type {}`.
573///
574/// `ValueConvertible` has default method implementations that use `platform_value`
575/// serialization, so the generated impl is always empty.
576///
577/// ```ignore
578/// #[derive(ValueConvertible)]
579/// pub enum ExtendedBlockInfo {
580///     V0(ExtendedBlockInfoV0),
581/// }
582/// ```
583#[proc_macro_derive(ValueConvertible)]
584pub fn derive_value_convertible(input: TokenStream) -> TokenStream {
585    let input = parse_macro_input!(input as DeriveInput);
586    let name = &input.ident;
587    let generics = &input.generics;
588    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
589
590    if let Data::Union(_) = &input.data {
591        return syn::Error::new_spanned(name, "ValueConvertible cannot be derived for unions")
592            .to_compile_error()
593            .into();
594    }
595
596    let expanded = quote! {
597        impl #impl_generics crate::serialization::ValueConvertible for #name #ty_generics #where_clause {}
598    };
599
600    expanded.into()
601}