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