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