Skip to main content

tronz_sol_macro/
lib.rs

1//! The [`tron_sol!`] procedural macro — a TRON-aware superset of alloy's `sol!`.
2//!
3//! `tron_sol!` forwards its entire input to `alloy_sol_types::sol!` to generate
4//! the Solidity type layer (`…Call` structs, events, errors, custom types,
5//! free-standing `struct`/`enum`/`type` definitions, …) and *additionally*
6//! generates a provider-bound `Instance` for every `contract`/`interface`
7//! carrying `#[sol(rpc)]`, wired to `tronz`'s `TronProvider`.
8//!
9//! It accepts everything `sol!` does for the inline-Solidity form, including:
10//!
11//! - **multiple items in one invocation** (several contracts, or contracts mixed with bare
12//!   `struct`/`enum`/`error`/`event`/`type` definitions);
13//! - **attribute passthrough**: any attribute other than the TRON-specific ones (`#[sol(rpc)]`,
14//!   `#[sol(bytecode = …)]`, `#[sol(deployed_bytecode = …)]`, `#[tron_sol(…)]`) is forwarded
15//!   verbatim to `sol!` — so `#[derive(…)]`, `#[sol(all_derives)]`, `#[sol(extra_derives(…))]`, doc
16//!   comments, etc. all work on the generated type layer.
17//!
18//! TRON-specific attributes:
19//!
20//! - `#[sol(rpc)]` — also generate a `TronProvider`-bound `Instance`.
21//! - `#[sol(bytecode = "0x…")]` — embed creation bytecode and generate `deploy_builder` / `deploy`
22//!   helpers (requires `#[sol(rpc)]`).
23//! - `#[sol(deployed_bytecode = "0x…")]` — embed the runtime bytecode as a `DEPLOYED_BYTECODE`
24//!   constant.
25//! - `#[tron_sol(tronz_crate = <path>)]` — override the runtime crate path.
26//!
27//! ```ignore
28//! // Type layer only (same as sol!) — bare types and multiple items are fine.
29//! tron_sol! {
30//!     struct Foo { uint256 x; }
31//!     enum Bar { A, B }
32//! }
33//!
34//! // Type layer + TRON RPC bindings, with derive passthrough.
35//! tron_sol! {
36//!     #[derive(Debug)]
37//!     #[sol(rpc)]
38//!     interface IERC20 {
39//!         function balanceOf(address owner) external view returns (uint256);
40//!         function transfer(address to, uint256 amount) external returns (bool);
41//!     }
42//! }
43//!
44//! let contract = IERC20::new(usdt_addr, provider);
45//! let balance = contract.balanceOf(owner).call().await?;
46//! ```
47
48use std::{
49    collections::HashMap,
50    hash::{DefaultHasher, Hash, Hasher},
51    iter::once,
52};
53
54use proc_macro::TokenStream;
55use proc_macro2::{
56    Delimiter, Group, Ident as Ident2, Punct, Spacing, Span, TokenStream as TokenStream2, TokenTree,
57};
58use quote::{format_ident, quote};
59use syn::{
60    Attribute, Ident, LitStr, Result,
61    parse::{Parse, ParseStream},
62    parse_macro_input,
63};
64use syn_solidity::{
65    FunctionKind, Item as SolItem, ItemContract, ItemEvent, ItemFunction, Spanned, Type as SolType,
66};
67
68/// A TRON-aware superset of alloy's `sol!`. See the [crate-level docs](crate).
69#[proc_macro]
70pub fn tron_sol(input: TokenStream) -> TokenStream {
71    let original = TokenStream2::from(input.clone());
72    let parsed = parse_macro_input!(input as TronSol);
73    match parsed.expand(original) {
74        Ok(ts) => ts.into(),
75        Err(e) => e.to_compile_error().into(),
76    }
77}
78
79struct TronSol {
80    items: Vec<SolItem>,
81    krate: TokenStream2,
82}
83
84impl Parse for TronSol {
85    fn parse(input: ParseStream<'_>) -> Result<Self> {
86        let mut items = Vec::new();
87        while !input.is_empty() {
88            items.push(input.parse::<SolItem>()?);
89        }
90
91        // `#[tron_sol(tronz_crate = <path>)]` may appear on any item; the last
92        // one wins. Defaults to the umbrella crate's `contract` module.
93        let mut krate: TokenStream2 = quote!(::tronz::contract);
94        for item in &items {
95            let Some(attrs) = item.attrs() else { continue };
96            for attr in attrs {
97                if attr.path().is_ident("tron_sol") {
98                    attr.parse_nested_meta(|meta| {
99                        if meta.path.is_ident("tronz_crate") {
100                            let path: syn::Path = meta.value()?.parse()?;
101                            krate = quote!(#path);
102                            Ok(())
103                        } else {
104                            Err(meta.error("unknown `tron_sol` option; expected `tronz_crate`"))
105                        }
106                    })?;
107                }
108            }
109        }
110
111        Ok(Self { items, krate })
112    }
113}
114
115impl TronSol {
116    fn expand(&self, original: TokenStream2) -> Result<TokenStream2> {
117        // All runtime paths go through `__private` to avoid a direct dependency
118        // on tronz-contract from this proc-macro crate.
119        let kpriv = {
120            let k = &self.krate;
121            quote!(#k::__private)
122        };
123        let alloy = quote!(#kpriv::alloy_sol_types);
124
125        // Compute a hash of the raw input before it is consumed, so the hidden
126        // types module gets a unique name regardless of contract names used.
127        let input_hash = {
128            let mut hasher = DefaultHasher::new();
129            original.to_string().hash(&mut hasher);
130            hasher.finish()
131        };
132
133        // The full type layer, with TRON-specific attributes stripped so alloy's
134        // `sol!` sees only what it understands.
135        let forwarded = strip_tron_attrs(original);
136
137        let mut rpc_contracts: Vec<&ItemContract> = Vec::new();
138        for item in &self.items {
139            if let SolItem::Contract(c) = item {
140                if contract_opts(&c.attrs)?.rpc {
141                    rpc_contracts.push(c);
142                }
143            }
144        }
145
146        // No RPC layer requested — emit the type layer directly (closest to a
147        // plain `sol!`), supporting multiple items and bare type definitions.
148        if rpc_contracts.is_empty() {
149            return Ok(quote! {
150                #alloy::sol! {
151                    #![sol(alloy_sol_types = #alloy)]
152                    #forwarded
153                }
154            });
155        }
156
157        // RPC layer requested. Put the whole type layer in a hidden module and
158        // re-export it; then add one `Instance` module per RPC contract. The
159        // explicit `pub mod <Name>` shadows the glob-imported contract module of
160        // the same name (an explicit item always shadows a glob import).
161        //
162        // The hidden module name includes a hash of the entire input so that
163        // multiple `tron_sol!` invocations in the same scope never collide, even
164        // when they happen to share the same first contract name.
165        let types_mod = format_ident!("__tron_sol_types_{:x}", input_hash);
166
167        let mut instances = Vec::new();
168        for c in &rpc_contracts {
169            instances.push(self.expand_contract(c, &kpriv, &types_mod)?);
170        }
171
172        Ok(quote! {
173            #[doc(hidden)]
174            #[allow(non_camel_case_types, non_snake_case, missing_docs, clippy::all)]
175            mod #types_mod {
176                #alloy::sol! {
177                    #![sol(alloy_sol_types = #alloy)]
178                    #forwarded
179                }
180            }
181
182            #[allow(unused_imports)]
183            pub use #types_mod::*;
184
185            #(#instances)*
186        })
187    }
188
189    /// Generate the provider-bound `Instance` module for one `#[sol(rpc)]`
190    /// contract.
191    fn expand_contract(
192        &self,
193        c: &ItemContract,
194        kpriv: &TokenStream2,
195        types_mod: &Ident,
196    ) -> Result<TokenStream2> {
197        let name = c.name.0.clone();
198        let opts = contract_opts(&c.attrs)?;
199
200        if opts.rename {
201            return Err(syn::Error::new(
202                c.name.span(),
203                "`#[sol(rename/rename_all)]` is not supported together with `#[sol(rpc)]`: \
204                 renaming the generated `…Call` types would desync the instance methods",
205            ));
206        }
207
208        let alloy = quote!(#kpriv::alloy_sol_types);
209        let aprim = quote!(#kpriv::alloy_primitives);
210        let taddr = quote!(#kpriv::tronz_primitives::Address);
211        let provider_tr = quote!(#kpriv::tronz_provider::TronProvider);
212        let cinst = quote!(#kpriv::ContractInstance);
213        let tcb = quote!(#kpriv::TronCallBuilder);
214        let tef = quote!(#kpriv::TronEventFilter);
215        let deploy_builder_ty = quote!(#kpriv::DeployBuilder);
216        let result_ty = quote!(#kpriv::Result);
217
218        // Split the contract body into callable functions, constructor, and events.
219        // Public state variables are converted to getter functions via
220        // `ItemFunction::from_variable_definition`, matching alloy's behaviour.
221        let mut functions: Vec<ItemFunction> = Vec::new();
222        let mut constructor: Option<ItemFunction> = None;
223        let mut events: Vec<ItemEvent> = Vec::new();
224        for item in &c.body {
225            match item {
226                SolItem::Function(f) => {
227                    if matches!(f.kind, FunctionKind::Function(_)) && f.name.is_some() {
228                        functions.push(f.clone());
229                    } else if matches!(f.kind, FunctionKind::Constructor(_)) {
230                        constructor = Some(f.clone());
231                    }
232                }
233                SolItem::Variable(v) => {
234                    // Public state variables expose a getter — convert to a synthetic
235                    // function and treat it exactly like an explicit `function` item.
236                    if v.attributes.visibility().is_some_and(|vis| vis.is_public()) {
237                        functions.push(ItemFunction::from_variable_definition(v.clone()));
238                    }
239                }
240                SolItem::Event(e) => {
241                    events.push(e.clone());
242                }
243                _ => {}
244            }
245        }
246
247        // Overloaded functions get a `_{i}` suffix, mirroring alloy's `sol!`.
248        let mut counts: HashMap<String, usize> = HashMap::new();
249        for f in &functions {
250            if let Some(n) = &f.name {
251                *counts.entry(n.to_string()).or_default() += 1;
252            }
253        }
254        let mut seen: HashMap<String, usize> = HashMap::new();
255        let methods = functions
256            .iter()
257            .map(|f| {
258                let base = f.name.as_ref().expect("name checked above").to_string();
259                let effective = if counts.get(&base).copied().unwrap_or(0) > 1 {
260                    let idx = seen.entry(base.clone()).or_insert(0);
261                    let e = format!("{base}_{idx}");
262                    *idx += 1;
263                    e
264                } else {
265                    base
266                };
267                let method_name = if is_reserved_method(&effective) {
268                    format!("{effective}_call")
269                } else {
270                    effective.clone()
271                };
272                expand_method(f, &effective, &method_name, &alloy, &aprim, &tcb)
273            })
274            .collect::<Result<Vec<_>>>()?;
275
276        // Runtime bytecode — only when `#[sol(deployed_bytecode = "0x…")]` is present.
277        let deployed_bytecode_tokens = match &opts.deployed_bytecode {
278            None => quote!(),
279            Some(bytes) => {
280                let byte_vals = bytes.iter().copied();
281                quote! {
282                    /// The runtime bytecode of this contract, as deployed on-chain.
283                    ///
284                    /// Can be compared against the output of `get_contract` to verify
285                    /// that the on-chain code matches the expected artifact.
286                    pub static DEPLOYED_BYTECODE: #aprim::Bytes =
287                        #aprim::Bytes::from_static(&[#(#byte_vals),*]);
288                }
289            }
290        };
291
292        // Deploy helpers — only when `#[sol(bytecode = "0x…")]` is present.
293        let (deploy_tokens, deploy_instance_tokens) = match &opts.bytecode {
294            None => (quote!(), quote!()),
295            Some(bytes) => {
296                let byte_vals = bytes.iter().copied();
297
298                let (ctor_decls, ctor_names, ctor_values) = match &constructor {
299                    None => (vec![], vec![], vec![]),
300                    Some(c) => collect_params(&c.parameters, &alloy, &aprim)?,
301                };
302
303                let ctor_sig = quote!(#(, #ctor_decls)*);
304                let ctor_fwd = quote!(#(, #ctor_names)*);
305                let bytecode_expr = if ctor_names.is_empty() {
306                    quote!(BYTECODE.clone())
307                } else {
308                    quote!(#aprim::Bytes::from(
309                        [
310                            &BYTECODE[..],
311                            &#alloy::SolConstructor::abi_encode(&constructorCall {
312                                #(#ctor_names: #ctor_values),*
313                            })[..]
314                        ].concat()
315                    ))
316                };
317
318                let free_fns = quote! {
319                    /// The creation bytecode of this contract.
320                    pub static BYTECODE: #aprim::Bytes =
321                        #aprim::Bytes::from_static(&[#(#byte_vals),*]);
322
323                    /// Create a [`DeployBuilder`] to deploy this contract.
324                    #[inline]
325                    pub fn deploy_builder<P: #provider_tr>(
326                        provider: P #ctor_sig
327                    ) -> #deploy_builder_ty<P> {
328                        Instance::<P>::deploy_builder(provider #ctor_fwd)
329                    }
330
331                    /// Deploy this contract and return a bound [`Instance`].
332                    #[inline]
333                    pub async fn deploy<P: #provider_tr + ::core::clone::Clone>(
334                        provider: P #ctor_sig
335                    ) -> #result_ty<Instance<P>> {
336                        Instance::<P>::deploy(provider #ctor_fwd).await
337                    }
338                };
339
340                let instance_methods = quote! {
341                    impl<P: #provider_tr + ::core::clone::Clone> Instance<P> {
342                        /// Create a [`DeployBuilder`] to deploy this contract.
343                        #[inline]
344                        pub fn deploy_builder(
345                            provider: P #ctor_sig
346                        ) -> #deploy_builder_ty<P> {
347                            #deploy_builder_ty::new(provider, #bytecode_expr)
348                        }
349
350                        /// Deploy this contract and return a bound [`Instance`].
351                        #[inline]
352                        pub async fn deploy(
353                            provider: P #ctor_sig
354                        ) -> #result_ty<Self> {
355                            let address = Self::deploy_builder(provider.clone() #ctor_fwd)
356                                .deploy()
357                                .await?;
358                            ::core::result::Result::Ok(new(address, provider))
359                        }
360                    }
361                };
362
363                (free_fns, instance_methods)
364            }
365        };
366
367        // Per-event filter methods, with overload suffixes mirroring alloy.
368        let mut ev_counts: HashMap<String, usize> = HashMap::new();
369        for e in &events {
370            *ev_counts.entry(e.name.to_string()).or_default() += 1;
371        }
372        let mut ev_seen: HashMap<String, usize> = HashMap::new();
373        let event_filter_methods = events
374            .iter()
375            .map(|e| {
376                let base = e.name.to_string();
377                let effective = if ev_counts.get(&base).copied().unwrap_or(0) > 1 {
378                    let idx = ev_seen.entry(base.clone()).or_insert(0);
379                    let s = format!("{base}_{idx}");
380                    *idx += 1;
381                    s
382                } else {
383                    base
384                };
385                let method_name = format_ident!("{}_filter", effective);
386                let event_ty = format_ident!("{}", effective);
387                let doc = format!("Creates an event filter for the [`{effective}`] event.");
388                quote! {
389                    #[doc = #doc]
390                    #[allow(non_snake_case)]
391                    pub fn #method_name(&self) -> #tef<P, #event_ty> {
392                        self.event_filter::<#event_ty>()
393                    }
394                }
395            })
396            .collect::<Vec<_>>();
397
398        Ok(quote! {
399            #[allow(non_snake_case, clippy::pub_underscore_fields)]
400            pub mod #name {
401                //! Provider-bound bindings generated by `tron_sol!`.
402                pub use super::#types_mod::#name::*;
403                // Bring the rest of the type layer (bare structs/enums, other
404                // contracts' types) into scope so custom-type parameters resolve.
405                #[allow(unused_imports)]
406                use super::#types_mod::*;
407
408                #deployed_bytecode_tokens
409
410                #deploy_tokens
411
412                /// A provider-bound handle to this contract.
413                #[derive(::core::clone::Clone)]
414                pub struct Instance<P: #provider_tr> {
415                    inner: #cinst<P>,
416                }
417
418                impl<P: #provider_tr> ::core::fmt::Debug for Instance<P> {
419                    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
420                        f.debug_struct(::core::stringify!(#name))
421                            .field("address", &self.inner.address())
422                            .finish()
423                    }
424                }
425
426                /// Bind to the contract at `address` over `provider`.
427                pub fn new<P: #provider_tr>(address: #taddr, provider: P) -> Instance<P> {
428                    Instance { inner: #cinst::new_raw(provider, address) }
429                }
430
431                impl<P: #provider_tr> Instance<P> {
432                    /// The contract address.
433                    #[inline]
434                    pub fn address(&self) -> #taddr {
435                        self.inner.address()
436                    }
437
438                    /// Sets the contract address.
439                    #[inline]
440                    pub fn set_address(&mut self, address: #taddr) {
441                        self.inner.set_address(address);
442                    }
443
444                    /// Return a new handle pointing at a different address.
445                    #[inline]
446                    pub fn at(mut self, address: #taddr) -> Self {
447                        self.set_address(address);
448                        self
449                    }
450
451                    /// Borrow the underlying provider.
452                    #[inline]
453                    pub fn provider(&self) -> &P {
454                        self.inner.provider()
455                    }
456
457                    /// Build a call for any [`SolCall`] type — generic entry point
458                    /// used by all typed methods.
459                    #[inline]
460                    pub fn call_builder<C: #alloy::SolCall>(&self, call: &C) -> #tcb<P, C> {
461                        #tcb::new(self.inner.call_raw(
462                            #alloy::SolCall::abi_encode(call).into()
463                        ))
464                    }
465
466                    #(#methods)*
467                }
468
469                impl<P: #provider_tr> Instance<P> {
470                    /// Build an event filter for any [`SolEvent`] type — generic
471                    /// entry point used by all per-event filter methods.
472                    #[inline]
473                    pub fn event_filter<E: #alloy::SolEvent>(&self) -> #tef<P, E> {
474                        #tef::new(self.inner.provider().clone(), Some(self.inner.address()))
475                    }
476
477                    #(#event_filter_methods)*
478                }
479
480                #deploy_instance_tokens
481            }
482        })
483    }
484}
485
486/// TRON-specific options parsed from a contract's `#[sol(...)]` attributes.
487#[derive(Default)]
488struct ContractOpts {
489    rpc: bool,
490    bytecode: Option<Vec<u8>>,
491    deployed_bytecode: Option<Vec<u8>>,
492    /// Whether `rename`/`rename_all` is present (incompatible with `rpc`).
493    rename: bool,
494}
495
496/// Read `rpc` / `bytecode` / `deployed_bytecode` / `rename(_all)` from the
497/// `#[sol(...)]` attributes of an item. Other options are left for alloy's
498/// `sol!` (they ride along via the forwarded token stream).
499fn contract_opts(attrs: &[Attribute]) -> Result<ContractOpts> {
500    let mut opts = ContractOpts::default();
501    for attr in attrs {
502        if !attr.path().is_ident("sol") {
503            continue;
504        }
505        attr.parse_nested_meta(|meta| {
506            if meta.path.is_ident("rpc") {
507                opts.rpc = if meta.input.peek(syn::Token![=]) {
508                    meta.value()?.parse::<syn::LitBool>()?.value
509                } else {
510                    true
511                };
512            } else if meta.path.is_ident("bytecode") {
513                opts.bytecode = Some(parse_hex(&meta.value()?.parse::<LitStr>()?)?);
514            } else if meta.path.is_ident("deployed_bytecode") {
515                opts.deployed_bytecode = Some(parse_hex(&meta.value()?.parse::<LitStr>()?)?);
516            } else {
517                if meta.path.is_ident("rename") || meta.path.is_ident("rename_all") {
518                    opts.rename = true;
519                }
520                // Unknown option (e.g. `all_derives`, `extra_derives(..)`,
521                // `rename_all = ".."`): consume any `= value` or `(tokens)` so
522                // the meta parser can continue. These ride along to alloy via
523                // the forwarded stream.
524                if meta.input.peek(syn::Token![=]) {
525                    let _: syn::Expr = meta.value()?.parse()?;
526                } else if meta.input.peek(syn::token::Paren) {
527                    let content;
528                    syn::parenthesized!(content in meta.input);
529                    let _: TokenStream2 = content.parse()?;
530                }
531            }
532            Ok(())
533        })?;
534    }
535    Ok(opts)
536}
537
538// ── token surgery: strip TRON-specific attributes before forwarding ──────────
539
540/// Remove the TRON-specific attributes (`#[tron_sol(...)]` and the
541/// `rpc`/`bytecode`/`deployed_bytecode` keys of `#[sol(...)]`) from a token
542/// stream, leaving everything else — including `#[derive(...)]` and the other
543/// `#[sol(...)]` keys — untouched, so the result can be fed to alloy's `sol!`.
544fn strip_tron_attrs(ts: TokenStream2) -> TokenStream2 {
545    let tokens: Vec<TokenTree> = ts.into_iter().collect();
546    let mut out = TokenStream2::new();
547    let mut i = 0;
548    while i < tokens.len() {
549        // Recurse into delimited groups (e.g. contract bodies).
550        if let TokenTree::Group(g) = &tokens[i] {
551            let mut ng = Group::new(g.delimiter(), strip_tron_attrs(g.stream()));
552            ng.set_span(g.span());
553            out.extend(once(TokenTree::Group(ng)));
554            i += 1;
555            continue;
556        }
557
558        // Attribute: `#` [`!`] `[ ... ]`
559        if let TokenTree::Punct(p) = &tokens[i] {
560            if p.as_char() == '#' {
561                let mut j = i + 1;
562                let bang = matches!(tokens.get(j), Some(TokenTree::Punct(q)) if q.as_char() == '!');
563                if bang {
564                    j += 1;
565                }
566                if let Some(TokenTree::Group(g)) = tokens.get(j) {
567                    if g.delimiter() == Delimiter::Bracket {
568                        // `None` => drop the whole attribute (emit nothing).
569                        if let Some(inner) = rewrite_attr(g.stream()) {
570                            out.extend(once(tokens[i].clone()));
571                            if bang {
572                                out.extend(once(tokens[i + 1].clone()));
573                            }
574                            let mut ng = Group::new(Delimiter::Bracket, inner);
575                            ng.set_span(g.span());
576                            out.extend(once(TokenTree::Group(ng)));
577                        }
578                        i = j + 1;
579                        continue;
580                    }
581                }
582            }
583        }
584
585        out.extend(once(tokens[i].clone()));
586        i += 1;
587    }
588    out
589}
590
591/// Rewrite the contents of a single `[ ... ]` attribute group.
592///
593/// Returns `None` to drop the attribute entirely, or `Some(tokens)` with the
594/// (possibly rewritten) inner tokens to keep.
595fn rewrite_attr(inner: TokenStream2) -> Option<TokenStream2> {
596    let toks: Vec<TokenTree> = inner.clone().into_iter().collect();
597    let lead = match toks.first() {
598        Some(TokenTree::Ident(id)) => id.to_string(),
599        _ => return Some(inner),
600    };
601
602    match lead.as_str() {
603        // `#[tron_sol(...)]` is TRON-only — drop it.
604        "tron_sol" => None,
605        // `#[sol(...)]` — strip the TRON-only keys, keep the rest.
606        "sol" => {
607            if let Some(TokenTree::Group(g)) = toks.get(1) {
608                if g.delimiter() == Delimiter::Parenthesis {
609                    let kept = filter_sol_meta(g.stream());
610                    if kept.is_empty() {
611                        return None;
612                    }
613                    let mut grp = Group::new(Delimiter::Parenthesis, kept);
614                    grp.set_span(g.span());
615                    return Some(
616                        [toks[0].clone(), TokenTree::Group(grp)]
617                            .into_iter()
618                            .collect(),
619                    );
620                }
621            }
622            Some(inner)
623        }
624        // Any other attribute (`derive`, `doc`, …) — keep verbatim.
625        _ => Some(inner),
626    }
627}
628
629/// Drop the `rpc` / `bytecode` / `deployed_bytecode` entries from the
630/// comma-separated meta list inside `#[sol(...)]`, preserving the rest.
631fn filter_sol_meta(stream: TokenStream2) -> TokenStream2 {
632    // Split into comma-separated items (commas inside nested groups are atomic).
633    let mut items: Vec<Vec<TokenTree>> = vec![Vec::new()];
634    for tt in stream {
635        if let TokenTree::Punct(p) = &tt {
636            if p.as_char() == ',' {
637                items.push(Vec::new());
638                continue;
639            }
640        }
641        items.last_mut().expect("non-empty").push(tt);
642    }
643
644    let mut out = TokenStream2::new();
645    let mut first = true;
646    for item in items {
647        if item.is_empty() {
648            continue;
649        }
650        let key = match item.first() {
651            Some(TokenTree::Ident(id)) => id.to_string(),
652            _ => String::new(),
653        };
654        if matches!(key.as_str(), "rpc" | "bytecode" | "deployed_bytecode") {
655            continue;
656        }
657        if !first {
658            out.extend(once(TokenTree::Punct(Punct::new(',', Spacing::Alone))));
659        }
660        first = false;
661        out.extend(item);
662    }
663    out
664}
665
666/// Solidity functions whose names collide with `Instance`'s own methods get a
667/// `_call` suffix, mirroring alloy's `call_builder_method_function_name`.
668fn is_reserved_method(name: &str) -> bool {
669    matches!(
670        name,
671        "new"
672            | "deploy"
673            | "deploy_builder"
674            | "address"
675            | "set_address"
676            | "at"
677            | "provider"
678            | "call_builder"
679            | "event_filter"
680    )
681}
682
683/// Generates one typed instance method.
684///
685/// `call_base` names the `…Call` struct; `method_name` may differ when
686/// `call_base` collides with a reserved `Instance` method.
687fn expand_method(
688    f: &ItemFunction,
689    call_base: &str,
690    method_name: &str,
691    alloy: &TokenStream2,
692    aprim: &TokenStream2,
693    tcb: &TokenStream2,
694) -> Result<TokenStream2> {
695    let fn_ident = format_ident!("{}", method_name);
696    let call_struct = format_ident!("{}Call", call_base);
697
698    let (decls, names, values) = collect_params(&f.parameters, alloy, aprim)?;
699
700    Ok(quote! {
701        #[allow(non_snake_case, clippy::too_many_arguments)]
702        pub fn #fn_ident(&self, #(#decls),*) -> #tcb<P, #call_struct> {
703            self.call_builder(&#call_struct { #(#names: #values),* })
704        }
705    })
706}
707
708/// Maps a parameter list to `(decls, names, values)`:
709/// - `decls`  — typed parameter declarations (`field: Type`)
710/// - `names`  — bare field idents, for forwarding and struct init LHS
711/// - `values` — value expressions (`Into::into(field)` or `field`), for struct init RHS and ABI
712///   encoding
713fn collect_params(
714    parameters: &syn_solidity::ParameterList,
715    alloy: &TokenStream2,
716    aprim: &TokenStream2,
717) -> Result<(Vec<TokenStream2>, Vec<Ident2>, Vec<TokenStream2>)> {
718    let mut decls = Vec::new();
719    let mut names = Vec::new();
720    let mut values = Vec::new();
721    for (i, var) in parameters.iter().enumerate() {
722        // Use the inner `Ident` directly so raw identifiers (e.g. `r#type`) and
723        // alloy's `self`→`this` rename are preserved without a round-trip through
724        // `to_string()` which would panic on the `r#` prefix.
725        let field: Ident2 = match &var.name {
726            Some(n) => n.0.clone(),
727            None => format_ident!("_{}", i),
728        };
729        match &var.ty {
730            SolType::Address(..) => {
731                decls.push(quote!(#field: impl ::core::convert::Into<#aprim::Address>));
732                names.push(field.clone());
733                values.push(quote!(::core::convert::Into::into(#field)));
734            }
735            other => {
736                let ty = rust_ty(other, alloy, aprim)?;
737                decls.push(quote!(#field: #ty));
738                names.push(field.clone());
739                values.push(quote!(#field));
740            }
741        }
742    }
743    Ok((decls, names, values))
744}
745
746/// Maps a Solidity type to the Rust type used in the `…Call` struct field.
747///
748/// Top-level `address` is handled by the caller as `impl Into<Address>`.
749fn rust_ty(ty: &SolType, alloy: &TokenStream2, aprim: &TokenStream2) -> Result<TokenStream2> {
750    let ts = match ty {
751        SolType::Bool(_) => quote!(bool),
752        // Only nested addresses reach here; top-level is handled by the caller.
753        SolType::Address(..) => quote!(#aprim::Address),
754        SolType::String(_) => quote!(::std::string::String),
755        SolType::Bytes(_) => quote!(#aprim::Bytes),
756        SolType::FixedBytes(_, n) => {
757            let n = n.get() as usize;
758            quote!(#aprim::FixedBytes<#n>)
759        }
760        SolType::Uint(_, size) => int_ty(size.map(|s| s.get()).unwrap_or(256), false, aprim),
761        SolType::Int(_, size) => int_ty(size.map(|s| s.get()).unwrap_or(256), true, aprim),
762        SolType::Array(arr) => {
763            let inner = rust_ty(&arr.ty, alloy, aprim)?;
764            match (&arr.size, arr.size_lit()) {
765                (None, _) => quote!(::std::vec::Vec<#inner>),
766                (Some(_), Some(lit)) => {
767                    let n: usize = lit.base10_parse()?;
768                    quote!([#inner; #n])
769                }
770                // Constant-expression sizes (e.g. `T[N]` where N is a Solidity
771                // `constant`) can't be evaluated here; refuse explicitly to avoid
772                // silently mismatching the `…Call` field type.
773                (Some(size), None) => {
774                    return Err(syn::Error::new(
775                        size.span(),
776                        "tron_sol! supports only integer-literal array sizes; \
777                         constant expressions are not evaluated here — use a \
778                         literal like `uint256[3]`",
779                    ));
780                }
781            }
782        }
783        SolType::Tuple(tuple) => {
784            let inners = tuple
785                .types
786                .iter()
787                .map(|t| rust_ty(t, alloy, aprim))
788                .collect::<Result<Vec<_>>>()?;
789            quote!((#(#inners,)*))
790        }
791        SolType::Custom(path) => {
792            // Defer to `<T as SolType>::RustType` so UDVTs resolve to their
793            // underlying type (e.g. `type Foo is uint256` → `U256`), matching
794            // the field type `sol!` generates in the `…Call` struct.
795            let id = path.last().0.clone();
796            quote!(<#id as #alloy::SolType>::RustType)
797        }
798        SolType::Function(_) | SolType::Mapping(_) => {
799            return Err(syn::Error::new(
800                ty.span(),
801                "`function` and `mapping` types are not supported as parameters",
802            ));
803        }
804    };
805    Ok(ts)
806}
807
808/// Maps `uintN`/`intN` to their Rust types, matching alloy's `sol!` convention:
809/// 8/16/32/64/128-bit → primitives, all others → `alloy_primitives::aliases`.
810fn int_ty(bits: u16, signed: bool, aprim: &TokenStream2) -> TokenStream2 {
811    let primitive = matches!(bits, 8 | 16 | 32 | 64 | 128);
812    if primitive {
813        let id = format_ident!("{}{}", if signed { "i" } else { "u" }, bits);
814        quote!(#id)
815    } else {
816        let id = format_ident!("{}{}", if signed { "I" } else { "U" }, bits);
817        quote!(#aprim::aliases::#id)
818    }
819}
820
821/// Decode a `#[sol(bytecode = "0x…")]` hex literal into raw bytes.
822fn parse_hex(lit: &LitStr) -> Result<Vec<u8>> {
823    let span = lit.span();
824    let s = lit.value();
825    let s = s
826        .strip_prefix("0x")
827        .or_else(|| s.strip_prefix("0X"))
828        .unwrap_or(&s);
829    if s.len() % 2 != 0 {
830        return Err(syn::Error::new(span, "bytecode hex string has odd length"));
831    }
832    s.as_bytes()
833        .chunks(2)
834        .map(|pair| {
835            let hi = hex_nibble(pair[0], span)?;
836            let lo = hex_nibble(pair[1], span)?;
837            Ok((hi << 4) | lo)
838        })
839        .collect()
840}
841
842fn hex_nibble(b: u8, span: Span) -> Result<u8> {
843    match b {
844        b'0'..=b'9' => Ok(b - b'0'),
845        b'a'..=b'f' => Ok(b - b'a' + 10),
846        b'A'..=b'F' => Ok(b - b'A' + 10),
847        _ => Err(syn::Error::new(
848            span,
849            format!("invalid hex byte '{}'", b as char),
850        )),
851    }
852}