use std::{
collections::HashMap,
hash::{DefaultHasher, Hash, Hasher},
iter::once,
};
use alloy_json_abi::ContractObject;
use proc_macro::TokenStream;
use proc_macro2::{
Delimiter, Group, Ident as Ident2, Punct, Spacing, Span, TokenStream as TokenStream2, TokenTree,
};
use quote::{format_ident, quote};
use syn::{
Attribute, Ident, LitStr, Result, Token,
parse::{Parse, ParseStream},
parse_macro_input,
};
use syn_solidity::{
File as SolFile, FunctionKind, Item as SolItem, ItemContract, ItemEvent, ItemFunction, Spanned,
Type as SolType,
};
#[proc_macro]
pub fn tron_sol(input: TokenStream) -> TokenStream {
let original = TokenStream2::from(input.clone());
let parsed = parse_macro_input!(input as TronSol);
match parsed.expand(original) {
Ok(ts) => ts.into(),
Err(e) => e.to_compile_error().into(),
}
}
struct TronSol {
items: Vec<SolItem>,
krate: TokenStream2,
pre_forwarded: Option<TokenStream2>,
include_path: Option<String>,
}
impl Parse for TronSol {
fn parse(input: ParseStream<'_>) -> Result<Self> {
{
let fork = input.fork();
let _ = Attribute::parse_inner(&fork);
let _ = Attribute::parse_outer(&fork);
if fork.peek(Ident) {
let inner = fork.fork();
if inner.parse::<Ident>().is_ok() && inner.peek(Token![,]) {
return Self::parse_json_abi(input);
}
}
}
let mut items = Vec::new();
while !input.is_empty() {
items.push(input.parse::<SolItem>()?);
}
let mut krate: TokenStream2 = quote!(::tronz::contract);
for item in &items {
let Some(attrs) = item.attrs() else { continue };
for attr in attrs {
if attr.path().is_ident("tron_sol") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("tronz_crate") {
let path: syn::Path = meta.value()?.parse()?;
krate = quote!(#path);
Ok(())
} else {
Err(meta.error("unknown `tron_sol` option; expected `tronz_crate`"))
}
})?;
}
}
}
Ok(Self { items, krate, pre_forwarded: None, include_path: None })
}
}
impl TronSol {
fn parse_json_abi(input: ParseStream<'_>) -> Result<Self> {
let inner_attrs = Attribute::parse_inner(input)?;
let attrs = Attribute::parse_outer(input)?;
let name: Ident = input.parse()?;
input.parse::<Token![,]>()?;
let path_lit: LitStr = input.parse()?;
let _ = input.parse::<Option<Token![,]>>()?;
let path_str = path_lit.value();
let (json, include_path) = {
let trimmed = path_str.trim();
if trimmed.starts_with('{') || trimmed.starts_with('[') {
(path_str.clone(), None)
} else {
let p = std::path::PathBuf::from(&path_str);
let abs = if p.is_relative() {
let dir =
std::env::var("CARGO_MANIFEST_DIR").map(std::path::PathBuf::from).map_err(
|_| syn::Error::new(path_lit.span(), "CARGO_MANIFEST_DIR is not set"),
)?;
dir.join(p)
} else {
p
};
let canonical = std::fs::canonicalize(&abs).map_err(|e| {
syn::Error::new(
path_lit.span(),
format!("failed to canonicalize `{path_str}`: {e}"),
)
})?;
let json = std::fs::read_to_string(&canonical).map_err(|e| {
syn::Error::new(path_lit.span(), format!("failed to read `{path_str}`: {e}"))
})?;
let abs_str = canonical.to_str().ok_or_else(|| {
syn::Error::new(path_lit.span(), "ABI file path is not valid UTF-8")
})?;
(json, Some(abs_str.to_owned()))
}
};
let obj = ContractObject::from_json(&json).map_err(|e| {
syn::Error::new(path_lit.span(), format!("invalid JSON ABI in `{path_str}`: {e}"))
})?;
let abi = obj.abi.ok_or_else(|| {
syn::Error::new(path_lit.span(), format!("`{path_str}` contains no `abi` field"))
})?;
let sol_ts: TokenStream2 = abi.to_sol(&name.to_string(), None).parse().map_err(|e| {
syn::Error::new(
path_lit.span(),
format!("ABI-generated Solidity failed to tokenize: {e}"),
)
})?;
let forwarded = quote! { #(#inner_attrs)* #sol_ts };
let full_ts = quote! { #(#inner_attrs)* #(#attrs)* #sol_ts };
let file: SolFile = syn::parse2(full_ts).map_err(|e| {
syn::Error::new(
path_lit.span(),
format!("failed to parse generated Solidity interface: {e}"),
)
})?;
let items = file.items;
let mut krate: TokenStream2 = quote!(::tronz::contract);
for item in &items {
let Some(item_attrs) = item.attrs() else { continue };
for attr in item_attrs {
if attr.path().is_ident("tron_sol") {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("tronz_crate") {
let path: syn::Path = meta.value()?.parse()?;
krate = quote!(#path);
Ok(())
} else {
Err(meta.error("unknown `tron_sol` option; expected `tronz_crate`"))
}
})?;
}
}
}
Ok(Self { items, krate, pre_forwarded: Some(forwarded), include_path })
}
fn expand(&self, original: TokenStream2) -> Result<TokenStream2> {
let kpriv = {
let k = &self.krate;
quote!(#k::__private)
};
let alloy = quote!(#kpriv::alloy_sol_types);
let input_hash = {
let mut hasher = DefaultHasher::new();
original.to_string().hash(&mut hasher);
hasher.finish()
};
let forwarded = match self.pre_forwarded.clone() {
Some(ts) => ts,
None => strip_tron_attrs(original),
};
let include = self.include_path.as_deref().map(|p| {
quote! { const _: &'static [u8] = ::core::include_bytes!(#p); }
});
let mut rpc_contracts: Vec<&ItemContract> = Vec::new();
for item in &self.items {
if let SolItem::Contract(c) = item
&& contract_opts(&c.attrs)?.rpc
{
rpc_contracts.push(c);
}
}
if rpc_contracts.is_empty() {
return Ok(quote! {
#include
#alloy::sol! {
#![sol(alloy_sol_types = #alloy)]
#forwarded
}
});
}
let types_mod = format_ident!("__tron_sol_types_{:x}", input_hash);
let mut instances = Vec::new();
for c in &rpc_contracts {
instances.push(self.expand_contract(c, &kpriv, &types_mod)?);
}
Ok(quote! {
#include
#[doc(hidden)]
#[allow(non_camel_case_types, non_snake_case, missing_docs, clippy::all)]
mod #types_mod {
#alloy::sol! {
#![sol(alloy_sol_types = #alloy)]
#forwarded
}
}
#[allow(unused_imports)]
pub use #types_mod::*;
#(#instances)*
})
}
fn expand_contract(
&self,
c: &ItemContract,
kpriv: &TokenStream2,
types_mod: &Ident,
) -> Result<TokenStream2> {
let name = c.name.0.clone();
let opts = contract_opts(&c.attrs)?;
if opts.rename {
return Err(syn::Error::new(
c.name.span(),
"`#[sol(rename/rename_all)]` is not supported together with `#[sol(rpc)]`: \
renaming the generated `…Call` types would desync the instance methods",
));
}
let alloy = quote!(#kpriv::alloy_sol_types);
let aprim = quote!(#kpriv::alloy_primitives);
let taddr = quote!(#kpriv::tronz_primitives::Address);
let provider_tr = quote!(#kpriv::tronz_provider::TronProvider);
let cinst = quote!(#kpriv::ContractInstance);
let tcb = quote!(#kpriv::TronCallBuilder);
let tef = quote!(#kpriv::TronEventFilter);
let deploy_builder_ty = quote!(#kpriv::DeployBuilder);
let result_ty = quote!(#kpriv::Result);
let mut functions: Vec<ItemFunction> = Vec::new();
let mut constructor: Option<ItemFunction> = None;
let mut events: Vec<ItemEvent> = Vec::new();
for item in &c.body {
match item {
SolItem::Function(f) => {
if matches!(f.kind, FunctionKind::Function(_)) && f.name.is_some() {
functions.push(f.clone());
} else if matches!(f.kind, FunctionKind::Constructor(_)) {
constructor = Some(f.clone());
}
}
SolItem::Variable(v) => {
if v.attributes.visibility().is_some_and(|vis| vis.is_public()) {
functions.push(ItemFunction::from_variable_definition(v.clone()));
}
}
SolItem::Event(e) => {
events.push(e.clone());
}
_ => {}
}
}
let mut counts: HashMap<String, usize> = HashMap::new();
for f in &functions {
if let Some(n) = &f.name {
*counts.entry(n.to_string()).or_default() += 1;
}
}
let mut seen: HashMap<String, usize> = HashMap::new();
let methods = functions
.iter()
.map(|f| {
let base = f.name.as_ref().expect("name checked above").to_string();
let effective = if counts.get(&base).copied().unwrap_or(0) > 1 {
let idx = seen.entry(base.clone()).or_insert(0);
let e = format!("{base}_{idx}");
*idx += 1;
e
} else {
base
};
let method_name = if is_reserved_method(&effective) {
format!("{effective}_call")
} else {
effective.clone()
};
expand_method(f, &effective, &method_name, &alloy, &aprim, &tcb)
})
.collect::<Result<Vec<_>>>()?;
let deployed_bytecode_tokens = match &opts.deployed_bytecode {
None => quote!(),
Some(bytes) => {
let byte_vals = bytes.iter().copied();
quote! {
pub static DEPLOYED_BYTECODE: #aprim::Bytes =
#aprim::Bytes::from_static(&[#(#byte_vals),*]);
}
}
};
let (deploy_tokens, deploy_instance_tokens) = match &opts.bytecode {
None => (quote!(), quote!()),
Some(bytes) => {
let byte_vals = bytes.iter().copied();
let (ctor_decls, ctor_names, ctor_values) = match &constructor {
None => (vec![], vec![], vec![]),
Some(c) => collect_params(&c.parameters, &alloy, &aprim)?,
};
let ctor_sig = quote!(#(, #ctor_decls)*);
let ctor_fwd = quote!(#(, #ctor_names)*);
let bytecode_expr = if ctor_names.is_empty() {
quote!(BYTECODE.clone())
} else {
quote!(#aprim::Bytes::from(
[
&BYTECODE[..],
&#alloy::SolConstructor::abi_encode(&constructorCall {
#(#ctor_names: #ctor_values),*
})[..]
].concat()
))
};
let free_fns = quote! {
pub static BYTECODE: #aprim::Bytes =
#aprim::Bytes::from_static(&[#(#byte_vals),*]);
#[inline]
pub fn deploy_builder<P: #provider_tr>(
provider: P #ctor_sig
) -> #deploy_builder_ty<P> {
Instance::<P>::deploy_builder(provider #ctor_fwd)
}
#[inline]
pub async fn deploy<P: #provider_tr + ::core::clone::Clone>(
provider: P #ctor_sig
) -> #result_ty<Instance<P>> {
Instance::<P>::deploy(provider #ctor_fwd).await
}
};
let instance_methods = quote! {
impl<P: #provider_tr + ::core::clone::Clone> Instance<P> {
#[inline]
pub fn deploy_builder(
provider: P #ctor_sig
) -> #deploy_builder_ty<P> {
#deploy_builder_ty::new(provider, #bytecode_expr)
}
#[inline]
pub async fn deploy(
provider: P #ctor_sig
) -> #result_ty<Self> {
let address = Self::deploy_builder(provider.clone() #ctor_fwd)
.deploy()
.await?;
::core::result::Result::Ok(new(address, provider))
}
}
};
(free_fns, instance_methods)
}
};
let mut ev_counts: HashMap<String, usize> = HashMap::new();
for e in &events {
*ev_counts.entry(e.name.to_string()).or_default() += 1;
}
let mut ev_seen: HashMap<String, usize> = HashMap::new();
let event_filter_methods = events
.iter()
.map(|e| {
let base = e.name.to_string();
let effective = if ev_counts.get(&base).copied().unwrap_or(0) > 1 {
let idx = ev_seen.entry(base.clone()).or_insert(0);
let s = format!("{base}_{idx}");
*idx += 1;
s
} else {
base
};
let method_name = format_ident!("{}_filter", effective);
let event_ty = format_ident!("{}", effective);
let doc = format!("Creates an event filter for the [`{effective}`] event.");
quote! {
#[doc = #doc]
#[allow(non_snake_case)]
pub fn #method_name(&self) -> #tef<P, #event_ty> {
self.event_filter::<#event_ty>()
}
}
})
.collect::<Vec<_>>();
Ok(quote! {
#[allow(non_snake_case, clippy::pub_underscore_fields)]
pub mod #name {
pub use super::#types_mod::#name::*;
#[allow(unused_imports)]
use super::#types_mod::*;
#deployed_bytecode_tokens
#deploy_tokens
#[derive(::core::clone::Clone)]
pub struct Instance<P: #provider_tr> {
inner: #cinst<P>,
}
impl<P: #provider_tr> ::core::fmt::Debug for Instance<P> {
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
f.debug_struct(::core::stringify!(#name))
.field("address", &self.inner.address())
.finish()
}
}
pub fn new<P: #provider_tr>(address: #taddr, provider: P) -> Instance<P> {
Instance { inner: #cinst::new_raw(provider, address) }
}
impl<P: #provider_tr> Instance<P> {
#[inline]
pub fn address(&self) -> #taddr {
self.inner.address()
}
#[inline]
pub fn set_address(&mut self, address: #taddr) {
self.inner.set_address(address);
}
#[inline]
pub fn at(mut self, address: #taddr) -> Self {
self.set_address(address);
self
}
#[inline]
pub fn provider(&self) -> &P {
self.inner.provider()
}
#[inline]
pub fn call_builder<C: #alloy::SolCall>(&self, call: &C) -> #tcb<P, C> {
#tcb::new(self.inner.call_raw(
#alloy::SolCall::abi_encode(call).into()
))
}
#(#methods)*
}
impl<P: #provider_tr> Instance<P> {
#[inline]
pub fn event_filter<E: #alloy::SolEvent>(&self) -> #tef<P, E> {
#tef::new(self.inner.provider().clone(), Some(self.inner.address()))
}
#(#event_filter_methods)*
}
#deploy_instance_tokens
}
})
}
}
#[derive(Default)]
struct ContractOpts {
rpc: bool,
bytecode: Option<Vec<u8>>,
deployed_bytecode: Option<Vec<u8>>,
rename: bool,
}
fn contract_opts(attrs: &[Attribute]) -> Result<ContractOpts> {
let mut opts = ContractOpts::default();
for attr in attrs {
if !attr.path().is_ident("sol") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rpc") {
opts.rpc = if meta.input.peek(syn::Token![=]) {
meta.value()?.parse::<syn::LitBool>()?.value
} else {
true
};
} else if meta.path.is_ident("bytecode") {
opts.bytecode = Some(parse_hex(&meta.value()?.parse::<LitStr>()?)?);
} else if meta.path.is_ident("deployed_bytecode") {
opts.deployed_bytecode = Some(parse_hex(&meta.value()?.parse::<LitStr>()?)?);
} else {
if meta.path.is_ident("rename") || meta.path.is_ident("rename_all") {
opts.rename = true;
}
if meta.input.peek(syn::Token![=]) {
let _: syn::Expr = meta.value()?.parse()?;
} else if meta.input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in meta.input);
let _: TokenStream2 = content.parse()?;
}
}
Ok(())
})?;
}
Ok(opts)
}
fn strip_tron_attrs(ts: TokenStream2) -> TokenStream2 {
let tokens: Vec<TokenTree> = ts.into_iter().collect();
let mut out = TokenStream2::new();
let mut i = 0;
while i < tokens.len() {
if let TokenTree::Group(g) = &tokens[i] {
let mut ng = Group::new(g.delimiter(), strip_tron_attrs(g.stream()));
ng.set_span(g.span());
out.extend(once(TokenTree::Group(ng)));
i += 1;
continue;
}
if let TokenTree::Punct(p) = &tokens[i]
&& p.as_char() == '#'
{
let mut j = i + 1;
let bang = matches!(tokens.get(j), Some(TokenTree::Punct(q)) if q.as_char() == '!');
if bang {
j += 1;
}
if let Some(TokenTree::Group(g)) = tokens.get(j)
&& g.delimiter() == Delimiter::Bracket
{
if let Some(inner) = rewrite_attr(g.stream()) {
out.extend(once(tokens[i].clone()));
if bang {
out.extend(once(tokens[i + 1].clone()));
}
let mut ng = Group::new(Delimiter::Bracket, inner);
ng.set_span(g.span());
out.extend(once(TokenTree::Group(ng)));
}
i = j + 1;
continue;
}
}
out.extend(once(tokens[i].clone()));
i += 1;
}
out
}
fn rewrite_attr(inner: TokenStream2) -> Option<TokenStream2> {
let toks: Vec<TokenTree> = inner.clone().into_iter().collect();
let lead = match toks.first() {
Some(TokenTree::Ident(id)) => id.to_string(),
_ => return Some(inner),
};
match lead.as_str() {
"tron_sol" => None,
"sol" => {
if let Some(TokenTree::Group(g)) = toks.get(1)
&& g.delimiter() == Delimiter::Parenthesis
{
let kept = filter_sol_meta(g.stream());
if kept.is_empty() {
return None;
}
let mut grp = Group::new(Delimiter::Parenthesis, kept);
grp.set_span(g.span());
return Some([toks[0].clone(), TokenTree::Group(grp)].into_iter().collect());
}
Some(inner)
}
_ => Some(inner),
}
}
fn filter_sol_meta(stream: TokenStream2) -> TokenStream2 {
let mut items: Vec<Vec<TokenTree>> = vec![Vec::new()];
for tt in stream {
if let TokenTree::Punct(p) = &tt
&& p.as_char() == ','
{
items.push(Vec::new());
continue;
}
items.last_mut().expect("non-empty").push(tt);
}
let mut out = TokenStream2::new();
let mut first = true;
for item in items {
if item.is_empty() {
continue;
}
let key = match item.first() {
Some(TokenTree::Ident(id)) => id.to_string(),
_ => String::new(),
};
if matches!(key.as_str(), "rpc" | "bytecode" | "deployed_bytecode") {
continue;
}
if !first {
out.extend(once(TokenTree::Punct(Punct::new(',', Spacing::Alone))));
}
first = false;
out.extend(item);
}
out
}
fn is_reserved_method(name: &str) -> bool {
matches!(
name,
"new"
| "deploy"
| "deploy_builder"
| "address"
| "set_address"
| "at"
| "provider"
| "call_builder"
| "event_filter"
)
}
fn expand_method(
f: &ItemFunction,
call_base: &str,
method_name: &str,
alloy: &TokenStream2,
aprim: &TokenStream2,
tcb: &TokenStream2,
) -> Result<TokenStream2> {
let fn_ident = format_ident!("{}", method_name);
let call_struct = format_ident!("{}Call", call_base);
let (decls, names, values) = collect_params(&f.parameters, alloy, aprim)?;
Ok(quote! {
#[allow(non_snake_case, clippy::too_many_arguments)]
pub fn #fn_ident(&self, #(#decls),*) -> #tcb<P, #call_struct> {
self.call_builder(&#call_struct { #(#names: #values),* })
}
})
}
fn collect_params(
parameters: &syn_solidity::ParameterList,
alloy: &TokenStream2,
aprim: &TokenStream2,
) -> Result<(Vec<TokenStream2>, Vec<Ident2>, Vec<TokenStream2>)> {
let mut decls = Vec::new();
let mut names = Vec::new();
let mut values = Vec::new();
for (i, var) in parameters.iter().enumerate() {
let field: Ident2 = match &var.name {
Some(n) => n.0.clone(),
None => format_ident!("_{}", i),
};
match &var.ty {
SolType::Address(..) => {
decls.push(quote!(#field: impl ::core::convert::Into<#aprim::Address>));
names.push(field.clone());
values.push(quote!(::core::convert::Into::into(#field)));
}
other => {
let ty = rust_ty(other, alloy, aprim)?;
decls.push(quote!(#field: #ty));
names.push(field.clone());
values.push(quote!(#field));
}
}
}
Ok((decls, names, values))
}
fn rust_ty(ty: &SolType, alloy: &TokenStream2, aprim: &TokenStream2) -> Result<TokenStream2> {
let ts = match ty {
SolType::Bool(_) => quote!(bool),
SolType::Address(..) => quote!(#aprim::Address),
SolType::String(_) => quote!(::std::string::String),
SolType::Bytes(_) => quote!(#aprim::Bytes),
SolType::FixedBytes(_, n) => {
let n = n.get() as usize;
quote!(#aprim::FixedBytes<#n>)
}
SolType::Uint(_, size) => int_ty(size.map(|s| s.get()).unwrap_or(256), false, aprim),
SolType::Int(_, size) => int_ty(size.map(|s| s.get()).unwrap_or(256), true, aprim),
SolType::Array(arr) => {
let inner = rust_ty(&arr.ty, alloy, aprim)?;
match (&arr.size, arr.size_lit()) {
(None, _) => quote!(::std::vec::Vec<#inner>),
(Some(_), Some(lit)) => {
let n: usize = lit.base10_parse()?;
quote!([#inner; #n])
}
(Some(size), None) => {
return Err(syn::Error::new(
size.span(),
"tron_sol! supports only integer-literal array sizes; \
constant expressions are not evaluated here — use a \
literal like `uint256[3]`",
));
}
}
}
SolType::Tuple(tuple) => {
let inners =
tuple.types.iter().map(|t| rust_ty(t, alloy, aprim)).collect::<Result<Vec<_>>>()?;
quote!((#(#inners,)*))
}
SolType::Custom(path) => {
let id = path.last().0.clone();
quote!(<#id as #alloy::SolType>::RustType)
}
SolType::Function(_) | SolType::Mapping(_) => {
return Err(syn::Error::new(
ty.span(),
"`function` and `mapping` types are not supported as parameters",
));
}
};
Ok(ts)
}
fn int_ty(bits: u16, signed: bool, aprim: &TokenStream2) -> TokenStream2 {
let primitive = matches!(bits, 8 | 16 | 32 | 64 | 128);
if primitive {
let id = format_ident!("{}{}", if signed { "i" } else { "u" }, bits);
quote!(#id)
} else {
let id = format_ident!("{}{}", if signed { "I" } else { "U" }, bits);
quote!(#aprim::aliases::#id)
}
}
fn parse_hex(lit: &LitStr) -> Result<Vec<u8>> {
let span = lit.span();
let s = lit.value();
let s = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(&s);
if !s.len().is_multiple_of(2) {
return Err(syn::Error::new(span, "bytecode hex string has odd length"));
}
s.as_bytes()
.chunks(2)
.map(|pair| {
let hi = hex_nibble(pair[0], span)?;
let lo = hex_nibble(pair[1], span)?;
Ok((hi << 4) | lo)
})
.collect()
}
fn hex_nibble(b: u8, span: Span) -> Result<u8> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'a'..=b'f' => Ok(b - b'a' + 10),
b'A'..=b'F' => Ok(b - b'A' + 10),
_ => Err(syn::Error::new(span, format!("invalid hex byte '{}'", b as char))),
}
}