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/// - `Option<SharedEncryptedNote>` / `Option<PrivateEncryptedNote>` (both
325///   `(u32, u32, Vec<u8>)` aliases) → `json::safe_integer::json_safe_option_encrypted_note`
326///   (3-tuple where the inner `Vec<u8>` is base64 in JSON HR)
327///
328/// When adding a new `type X = u64` alias in rs-dpp, add it to the appropriate list below.
329fn serde_with_suffix_for_type(ty: &Type) -> Option<&'static str> {
330    match ty {
331        // [u8; N] — any fixed-size byte array, length-agnostic via const generics.
332        Type::Array(arr) if is_u8_type(&arr.elem) => Some("serde_bytes"),
333        Type::Path(type_path) => {
334            let segments = &type_path.path.segments;
335            // Get the last segment — handles both `u64` and `std::u64` / `crate::prelude::Credits`
336            let last = segments.last()?;
337            let ident = &last.ident;
338
339            if is_u64_type(ident) {
340                return Some("json_safe_u64");
341            }
342            if is_i64_type(ident) {
343                return Some("json_safe_i64");
344            }
345            // Vec<u8> → variable-length bytes helper (base64 in JSON, raw in binary).
346            if ident == "Vec" {
347                if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
348                    if args.args.len() == 1 {
349                        if let syn::GenericArgument::Type(inner_ty) = &args.args[0] {
350                            if is_u8_type(inner_ty) {
351                                return Some("serde_bytes_var");
352                            }
353                        }
354                    }
355                }
356            }
357            // Check for Option<u64/i64/alias> — handles both `Option<T>` and `std::option::Option<T>`
358            if ident == "Option" {
359                if let syn::PathArguments::AngleBracketed(args) = &last.arguments {
360                    if args.args.len() == 1 {
361                        if let syn::GenericArgument::Type(Type::Path(inner)) = &args.args[0] {
362                            if let Some(inner_last) = inner.path.segments.last() {
363                                if is_u64_type(&inner_last.ident) {
364                                    return Some("json_safe_option_u64");
365                                }
366                                if is_i64_type(&inner_last.ident) {
367                                    return Some("json_safe_option_i64");
368                                }
369                                if is_encrypted_note_alias(&inner_last.ident) {
370                                    return Some(
371                                        "json::safe_integer::json_safe_option_encrypted_note",
372                                    );
373                                }
374                            }
375                        }
376                    }
377                }
378            }
379            None
380        }
381        _ => None,
382    }
383}
384
385/// Known type aliases that resolve to u64.
386/// Keep in sync with type aliases defined in rs-dpp.
387const U64_ALIASES: &[&str] = &[
388    "BlockHeight",
389    "BlockHeightInterval",
390    "Credits",
391    "Duffs",
392    "FeeMultiplier",
393    "IdentityNonce",
394    "ProtocolVersionVoteCount",
395    "RemainingCredits",
396    "Revision",
397    "TimestampMillis",
398    "TimestampMillisInterval",
399    "TokenAmount",
400    "TokenDistributionWeight",
401    "WithdrawalTransactionIndex",
402];
403
404/// Known type aliases that resolve to i64.
405const I64_ALIASES: &[&str] = &["SignedCredits", "SignedTokenAmount"];
406
407/// Known type aliases that resolve to `(u32, u32, Vec<u8>)` (encrypted-note
408/// shape on token transitions). Routed to
409/// `json_safe_option_encrypted_note` so the inner `Vec<u8>` is base64 in
410/// JSON HR. Add new aliases of the same shape here.
411const ENCRYPTED_NOTE_ALIASES: &[&str] = &["SharedEncryptedNote", "PrivateEncryptedNote"];
412
413fn is_encrypted_note_alias(ident: &Ident) -> bool {
414    ENCRYPTED_NOTE_ALIASES.iter().any(|alias| ident == alias)
415}
416
417/// Check if the struct has serde derives (Serialize or Deserialize) in its attributes.
418///
419/// NOTE: `cfg_attr` is evaluated by the compiler BEFORE attribute macros run.
420/// If serde derives are behind `cfg_attr(feature = "...", derive(Serialize))` and the
421/// feature is disabled, the `cfg_attr` is already stripped — so we won't see it, which
422/// is the correct behavior (we shouldn't add `#[serde(with)]` when serde isn't derived).
423fn has_serde_derive(attrs: &[syn::Attribute]) -> bool {
424    for attr in attrs {
425        if attr.path().is_ident("derive") {
426            if let Ok(list) = attr.meta.require_list() {
427                // Parse the derive list to find exact Serialize/Deserialize idents
428                // Must handle both `Serialize` and `serde::Serialize` forms
429                let tokens = list.tokens.clone();
430                for tt in tokens.into_iter() {
431                    if let proc_macro2::TokenTree::Ident(ident) = &tt {
432                        let name = ident.to_string();
433                        // Exact ident match — "PlatformSerialize" is a single ident,
434                        // not "Platform" + "Serialize", so this won't false-match.
435                        if name == "Serialize" || name == "Deserialize" {
436                            return true;
437                        }
438                    }
439                }
440            }
441        }
442    }
443    false
444}
445
446/// Check if a field already has `#[serde(default)]` or `#[serde(default = "...")]`
447/// in any of its attributes, including inside `#[cfg_attr(..., serde(..., default))]`.
448///
449/// Note: cfg_attr on fields inside a struct body is NOT evaluated before
450/// the outer attribute macro processes the struct, so we must scan tokens manually.
451fn has_serde_default(attrs: &[syn::Attribute]) -> bool {
452    for attr in attrs {
453        let full_tokens = quote! { #attr }.to_string();
454        // Check if this attribute contains "serde" and a "default" token
455        if full_tokens.contains("serde") && full_tokens.contains("default") {
456            if let Ok(list) = attr.meta.require_list() {
457                let token_str = list.tokens.to_string();
458                // Match "default" as standalone or "default = ..." (custom fn)
459                if has_default_in_token_str(&token_str) {
460                    return true;
461                }
462                // Also check inside nested groups (cfg_attr case)
463                // e.g., cfg_attr(feature = "...", serde(rename = "...", default))
464                for tt in list.tokens.clone() {
465                    if let proc_macro2::TokenTree::Group(group) = tt {
466                        let inner = group.stream().to_string();
467                        if has_default_in_token_str(&inner) {
468                            return true;
469                        }
470                    }
471                }
472            }
473        }
474    }
475    false
476}
477
478/// Check if a comma-separated token string contains a "default" item,
479/// either standalone (`default`) or with a custom function (`default = "..."`).
480fn has_default_in_token_str(s: &str) -> bool {
481    s.split(',').any(|part| {
482        let trimmed = part.trim();
483        trimmed == "default" || trimmed.starts_with("default =") || trimmed.starts_with("default=")
484    })
485}
486
487fn is_u64_type(ident: &Ident) -> bool {
488    ident == "u64" || U64_ALIASES.iter().any(|alias| ident == alias)
489}
490
491fn is_i64_type(ident: &Ident) -> bool {
492    ident == "i64" || I64_ALIASES.iter().any(|alias| ident == alias)
493}
494
495/// True when the type resolves to `u8` — used to detect `[u8; N]` and `Vec<u8>`
496/// byte fields and auto-inject the matching `serde_bytes_*` helper.
497fn is_u8_type(ty: &Type) -> bool {
498    if let Type::Path(type_path) = ty {
499        if let Some(last) = type_path.path.segments.last() {
500            return last.ident == "u8" && last.arguments.is_empty();
501        }
502    }
503    false
504}
505
506/// Derive macro that generates `impl JsonConvertible for Type {}` with
507/// compile-time assertions that all inner types implement `JsonSafeFields`.
508///
509/// Feature gates are handled externally via `cfg_attr`:
510/// ```ignore
511/// #[cfg_attr(feature = "json-conversion", derive(JsonConvertible))]
512/// #[serde(tag = "$formatVersion")]
513/// pub enum TokenConfiguration {
514///     #[serde(rename = "0")]
515///     V0(TokenConfigurationV0),
516/// }
517/// ```
518#[proc_macro_derive(JsonConvertible)]
519pub fn derive_json_convertible(input: TokenStream) -> TokenStream {
520    let input = parse_macro_input!(input as DeriveInput);
521    let name = &input.ident;
522    let generics = &input.generics;
523    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
524
525    // Collect inner types from enum variants for compile-time assertions
526    let assertions = match &input.data {
527        Data::Enum(data_enum) => {
528            let mut assertions = Vec::new();
529            for variant in &data_enum.variants {
530                if let Fields::Unnamed(fields) = &variant.fields {
531                    for (i, field) in fields.unnamed.iter().enumerate() {
532                        let ty = &field.ty;
533                        let assert_fn_name = Ident::new(
534                            &format!(
535                                "must_have_json_safe_fields_attribute_{}_{}",
536                                variant.ident.to_string().to_lowercase(),
537                                i
538                            ),
539                            Span::call_site(),
540                        );
541                        assertions.push(quote! {
542                            fn #assert_fn_name<T: crate::serialization::JsonSafeFields>() {}
543                            let _ = #assert_fn_name::<#ty>;
544                        });
545                    }
546                }
547            }
548            assertions
549        }
550        Data::Struct(_) => {
551            // For plain structs, assert that Self implements JsonSafeFields
552            vec![quote! {
553                fn must_have_json_safe_fields_attribute<T: crate::serialization::JsonSafeFields>() {}
554                let _ = must_have_json_safe_fields_attribute::<#name #ty_generics>;
555            }]
556        }
557        Data::Union(_) => {
558            return syn::Error::new_spanned(name, "JsonConvertible cannot be derived for unions")
559                .to_compile_error()
560                .into();
561        }
562    };
563
564    let json_safe_fields_impl = match &input.data {
565        Data::Enum(_) => {
566            // Enums get JsonSafeFields — their inner V0 types are verified below.
567            quote! {
568                impl #impl_generics crate::serialization::JsonSafeFields for #name #ty_generics #where_clause {}
569            }
570        }
571        _ => {
572            // Structs already have JsonSafeFields from #[json_safe_fields] — don't duplicate.
573            quote! {}
574        }
575    };
576
577    let expanded = quote! {
578        impl #impl_generics crate::serialization::JsonConvertible for #name #ty_generics #where_clause {}
579
580        #json_safe_fields_impl
581
582        const _: () = {
583            #(#assertions)*
584        };
585    };
586
587    expanded.into()
588}
589
590/// Derive macro that generates `impl ValueConvertible for Type {}`.
591///
592/// `ValueConvertible` has default method implementations that use `platform_value`
593/// serialization, so the generated impl is always empty.
594///
595/// ```ignore
596/// #[derive(ValueConvertible)]
597/// pub enum ExtendedBlockInfo {
598///     V0(ExtendedBlockInfoV0),
599/// }
600/// ```
601#[proc_macro_derive(ValueConvertible)]
602pub fn derive_value_convertible(input: TokenStream) -> TokenStream {
603    let input = parse_macro_input!(input as DeriveInput);
604    let name = &input.ident;
605    let generics = &input.generics;
606    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
607
608    if let Data::Union(_) = &input.data {
609        return syn::Error::new_spanned(name, "ValueConvertible cannot be derived for unions")
610            .to_compile_error()
611            .into();
612    }
613
614    let expanded = quote! {
615        impl #impl_generics crate::serialization::ValueConvertible for #name #ty_generics #where_clause {}
616    };
617
618    expanded.into()
619}