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}