alloy_tx_macros/
lib.rs

1#![doc = include_str!("../README.md")]
2#![doc(
3    html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg",
4    html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico"
5)]
6#![cfg_attr(not(test), warn(unused_crate_dependencies))]
7#![cfg_attr(docsrs, feature(doc_cfg))]
8#![allow(clippy::option_if_let_else)]
9
10mod expand;
11mod parse;
12mod serde;
13
14use expand::Expander;
15use parse::{EnvelopeArgs, GroupedVariants};
16use proc_macro::TokenStream;
17use quote::quote;
18use syn::{parse_macro_input, parse_quote, DeriveInput, Error, Ident};
19
20/// Derive macro for creating transaction envelope types.
21///
22/// This macro generates a transaction envelope implementation that supports
23/// multiple transaction types following the EIP-2718 standard.
24///
25/// # Container Attributes
26///
27/// - `#[envelope(tx_type_name = MyTxType)]` - Custom name for the generated transaction type enum
28/// - `#[envelope(alloy_consensus = path::to::alloy)]` - Custom path to alloy_consensus crate
29/// - `#[envelope(typed = MyTypedTransaction)]` - Generate a corresponding TypedTransaction enum
30///   (optional)
31///
32/// # Variant Attributes
33/// - Each variant must be annotated with `envelope` attribute with one of the following options:
34///   - `#[envelope(ty = N)]` - Specify the transaction type ID (0-255)
35///   - `#[envelope(ty = N, typed = CustomType)]` - Use a custom transaction type for this variant
36///     in the generated TypedTransaction (optional)
37///   - `#[envelope(flatten)]` - Flatten this variant to delegate to inner envelope type
38///
39/// # Generated Code
40///
41/// The macro generates:
42/// - A `MyTxType` enum with transaction type variants
43/// - Implementations of `Transaction`, `Typed2718`, `Encodable2718`, `Decodable2718`
44/// - Serde serialization/deserialization support (if `serde` feature is enabled)
45/// - Arbitrary implementations (if `arbitrary` feature is enabled)
46/// - Optionally, a TypedTransaction enum (if `typed` attribute is specified)
47#[proc_macro_derive(TransactionEnvelope, attributes(envelope, serde))]
48pub fn derive_transaction_envelope(input: TokenStream) -> TokenStream {
49    let input = parse_macro_input!(input as DeriveInput);
50
51    match expand_transaction_envelope(input) {
52        Ok(tokens) => tokens.into(),
53        Err(err) => err.to_compile_error().into(),
54    }
55}
56
57/// Expand the transaction envelope derive macro.
58fn expand_transaction_envelope(input: syn::DeriveInput) -> Result<proc_macro2::TokenStream, Error> {
59    use darling::FromDeriveInput;
60
61    // Parse the input with darling
62    let args = EnvelopeArgs::from_derive_input(&input)
63        .map_err(|e| Error::new_spanned(&input.ident, e.to_string()))?;
64
65    // Extract config values before consuming args
66    let input_type_name = args.ident.clone();
67    let tx_type_enum_name = args
68        .tx_type_name
69        .clone()
70        .unwrap_or_else(|| Ident::new(&format!("{input_type_name}Type"), input_type_name.span()));
71    let alloy_consensus =
72        args.alloy_consensus.clone().unwrap_or_else(|| parse_quote!(::alloy_consensus));
73    let generics = args.generics.clone();
74    let typed = args.typed.clone();
75    let serde_cfg = match args.serde_cfg.as_ref() {
76        Some(syn::Meta::List(list)) => list.tokens.clone(),
77        Some(_) => {
78            return Err(Error::new_spanned(
79                &input.ident,
80                "serde_cfg must be a list like `serde_cfg(feature = \"serde\")`",
81            ))
82        }
83        // this is always true
84        None => quote! { all() },
85    };
86
87    let arbitrary_cfg = match args.arbitrary_cfg.as_ref() {
88        Some(syn::Meta::List(list)) => list.tokens.clone(),
89        Some(_) => {
90            return Err(Error::new_spanned(
91                &input.ident,
92                "arbitrary_cfg must be a list like `arbitrary_cfg(feature = \"arbitrary\")`",
93            ))
94        }
95        None => quote! { all() },
96    };
97
98    let variants = GroupedVariants::from_args(args)?;
99
100    let alloy_primitives = quote! { #alloy_consensus::private::alloy_primitives };
101    let alloy_eips = quote! { #alloy_consensus::private::alloy_eips };
102    let alloy_rlp = quote! { #alloy_consensus::private::alloy_rlp };
103
104    // Expand the macro
105    let expander = Expander {
106        input_type_name,
107        tx_type_enum_name,
108        alloy_consensus,
109        generics,
110        serde_enabled: cfg!(feature = "serde"),
111        serde_cfg,
112        arbitrary_cfg,
113        arbitrary_enabled: cfg!(feature = "arbitrary"),
114        alloy_primitives,
115        alloy_eips,
116        alloy_rlp,
117        variants,
118        typed,
119    };
120    Ok(expander.expand())
121}