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}