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}