diff --git a/crates/sol-macro/src/attr.rs b/crates/sol-macro/src/attr.rs index 1cd51a960..14ee02ead 100644 --- a/crates/sol-macro/src/attr.rs +++ b/crates/sol-macro/src/attr.rs @@ -69,6 +69,7 @@ pub struct SolAttrs { pub all_derives: Option, pub extra_methods: Option, pub docs: Option, + pub abi: Option, // TODO: Implement pub rename: Option, @@ -138,6 +139,7 @@ impl SolAttrs { all_derives => bool()?, extra_methods => bool()?, docs => bool()?, + abi => bool()?, rename => lit()?, rename_all => CasingStyle::from_lit(&lit()?)?, @@ -297,6 +299,10 @@ mod tests { #[sol(docs)] => Ok(sol_attrs! { docs: true }), #[sol(docs = true)] => Ok(sol_attrs! { docs: true }), #[sol(docs = false)] => Ok(sol_attrs! { docs: false }), + + #[sol(abi)] => Ok(sol_attrs! { abi: true }), + #[sol(abi = true)] => Ok(sol_attrs! { abi: true }), + #[sol(abi = false)] => Ok(sol_attrs! { abi: false }), } rename { diff --git a/crates/sol-macro/src/expand/contract.rs b/crates/sol-macro/src/expand/contract.rs index f6406bf7e..9d0283098 100644 --- a/crates/sol-macro/src/expand/contract.rs +++ b/crates/sol-macro/src/expand/contract.rs @@ -2,9 +2,9 @@ use super::{ty, ExpCtxt}; use crate::{attr, utils::ExprArray}; -use ast::{Item, ItemContract, ItemError, ItemEvent, ItemFunction, SolIdent}; +use ast::{Item, ItemContract, ItemError, ItemEvent, ItemFunction, SolIdent, Spanned}; use heck::ToSnakeCase; -use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro2::{Ident, TokenStream}; use quote::{format_ident, quote}; use syn::{parse_quote, Attribute, Result}; @@ -33,6 +33,7 @@ pub(super) fn expand(cx: &ExpCtxt<'_>, contract: &ItemContract) -> Result, contract: &ItemContract) -> Result, _) = attrs.into_iter().partition(|a| a.path().is_ident("doc")); mod_attrs.extend(item_attrs.iter().filter(|a| !a.path().is_ident("derive")).cloned()); + let mut item_tokens = TokenStream::new(); for item in body { match item { - Item::Function(function) if function.name.is_some() => functions.push(function), + Item::Function(function) => match function.kind { + ast::FunctionKind::Function(_) if function.name.is_some() => { + functions.push(function); + } + ast::FunctionKind::Constructor(_) => { + if constructor.is_none() { + constructor = Some(function); + } else { + let msg = "duplicate constructor"; + return Err(syn::Error::new(function.span(), msg)); + } + } + ast::FunctionKind::Fallback(_) => { + if fallback.is_none() { + fallback = Some(function); + } else { + let msg = "duplicate fallback function"; + return Err(syn::Error::new(function.span(), msg)); + } + } + ast::FunctionKind::Receive(_) => { + if receive.is_none() { + receive = Some(function); + } else { + let msg = "duplicate receive function"; + return Err(syn::Error::new(function.span(), msg)); + } + } + _ => {} + }, Item::Error(error) => errors.push(error), Item::Event(event) => events.push(event), _ => {} @@ -77,31 +110,96 @@ pub(super) fn expand(cx: &ExpCtxt<'_>, contract: &ItemContract) -> Result json::JsonAbi { + json::JsonAbi { + constructor: constructor(), + fallback: fallback(), + receive: receive(), + functions: functions(), + events: events(), + errors: errors(), + } + } + + /// Returns the [`Constructor`](json::Constructor) of [this contract](super), if any. + pub fn constructor() -> Option { + #constructor + } + + /// Returns the [`Fallback`](json::Fallback) function of [this contract](super), if any. + pub fn fallback() -> Option { + #fallback + } + + /// Returns the [`Receive`](json::Receive) function of [this contract](super), if any. + pub fn receive() -> Option { + #receive + } + + /// Returns a map of all the [`Function`](json::Function)s of [this contract](super). + pub fn functions() -> BTreeMap> { + #functions_map + } + + /// Returns a map of all the [`Event`](json::Event)s of [this contract](super). + pub fn events() -> BTreeMap> { + #events_map + } + + /// Returns a map of all the [`Error`](json::Error)s of [this contract](super). + pub fn errors() -> BTreeMap> { + #errors_map + } + } + } + } + }); + let tokens = quote! { #mod_descr_doc #(#mod_attrs)* @@ -118,6 +216,8 @@ pub(super) fn expand(cx: &ExpCtxt<'_>, contract: &ItemContract) -> Result, contract: &ItemContract) -> Result { cx: &'a ExpCtxt<'a>, + contract_name: SolIdent, + extra_methods: bool, +} + +struct ExpandData { name: Ident, variants: Vec, + types: Option>, min_data_len: usize, trait_: Ident, - data: CallLikeExpanderData, + selectors: Vec>, } -enum CallLikeExpanderData { - Function { selectors: Vec>, types: Vec }, - Error { selectors: Vec> }, - Event { selectors: Vec> }, +impl ExpandData { + fn types(&self) -> &Vec { + let types = self.types.as_ref().unwrap_or(&self.variants); + assert_eq!(types.len(), self.variants.len()); + types + } } -impl<'a> CallLikeExpander<'a> { - fn from_functions( - cx: &'a ExpCtxt<'a>, - contract_name: &SolIdent, - functions: Vec<&ItemFunction>, - ) -> Self { - let variants: Vec<_> = functions.iter().map(|&f| cx.overloaded_name(f.into()).0).collect(); - - let types: Vec<_> = variants.iter().map(|name| cx.raw_call_name(name)).collect(); - - let mut selectors: Vec<_> = functions.iter().map(|f| cx.function_selector(f)).collect(); - selectors.sort_unstable_by_key(|a| a.array); - - Self { - cx, - name: format_ident!("{contract_name}Calls"), - variants, - min_data_len: functions - .iter() - .map(|function| ty::params_base_data_size(cx, &function.arguments)) - .min() - .unwrap(), - trait_: Ident::new("SolCall", Span::call_site()), - data: CallLikeExpanderData::Function { selectors, types }, - } - } +enum ToExpand<'a> { + Functions(&'a [&'a ItemFunction]), + Errors(&'a [&'a ItemError]), + Events(&'a [&'a ItemEvent]), +} - fn from_errors(cx: &'a ExpCtxt<'a>, contract_name: &SolIdent, errors: Vec<&ItemError>) -> Self { - let mut selectors: Vec<_> = errors.iter().map(|e| cx.error_selector(e)).collect(); - selectors.sort_unstable_by_key(|a| a.array); - - Self { - cx, - name: format_ident!("{contract_name}Errors"), - variants: errors.iter().map(|error| error.name.0.clone()).collect(), - min_data_len: errors - .iter() - .map(|error| ty::params_base_data_size(cx, &error.parameters)) - .min() - .unwrap(), - trait_: Ident::new("SolError", Span::call_site()), - data: CallLikeExpanderData::Error { selectors }, - } - } +impl<'a> ToExpand<'a> { + fn to_data(&self, expander: &CallLikeExpander<'_>) -> ExpandData { + let &CallLikeExpander { cx, ref contract_name, .. } = expander; + match self { + Self::Functions(functions) => { + let variants: Vec<_> = + functions.iter().map(|&f| cx.overloaded_name(f.into()).0).collect(); + + let types: Vec<_> = variants.iter().map(|name| cx.raw_call_name(name)).collect(); + + let mut selectors: Vec<_> = + functions.iter().map(|f| cx.function_selector(f)).collect(); + selectors.sort_unstable(); + + ExpandData { + name: format_ident!("{contract_name}Calls"), + variants, + types: Some(types), + min_data_len: functions + .iter() + .map(|function| ty::params_base_data_size(cx, &function.parameters)) + .min() + .unwrap(), + trait_: format_ident!("SolCall"), + selectors, + } + } - fn from_events(cx: &'a ExpCtxt<'a>, contract_name: &SolIdent, events: Vec<&ItemEvent>) -> Self { - let variants: Vec<_> = - events.iter().map(|&event| cx.overloaded_name(event.into()).0).collect(); - - let mut selectors: Vec<_> = events.iter().map(|e| cx.event_selector(e)).collect(); - selectors.sort_unstable_by_key(|a| a.array); - - Self { - cx, - name: format_ident!("{contract_name}Events"), - variants, - min_data_len: events - .iter() - .map(|event| ty::params_base_data_size(cx, &event.params())) - .min() - .unwrap(), - trait_: Ident::new("SolEvent", Span::call_site()), - data: CallLikeExpanderData::Event { selectors }, - } - } + Self::Errors(errors) => { + let mut selectors: Vec<_> = errors.iter().map(|e| cx.error_selector(e)).collect(); + selectors.sort_unstable(); + + ExpandData { + name: format_ident!("{contract_name}Errors"), + variants: errors.iter().map(|error| error.name.0.clone()).collect(), + types: None, + min_data_len: errors + .iter() + .map(|error| ty::params_base_data_size(cx, &error.parameters)) + .min() + .unwrap(), + trait_: format_ident!("SolError"), + selectors, + } + } - /// Type name overrides. Currently only functions support because of the - /// `Call` suffix. - fn types(&self) -> &[Ident] { - match &self.data { - CallLikeExpanderData::Function { types, .. } => types, - _ => &self.variants, + Self::Events(events) => { + let variants: Vec<_> = + events.iter().map(|&event| cx.overloaded_name(event.into()).0).collect(); + + let mut selectors: Vec<_> = events.iter().map(|e| cx.event_selector(e)).collect(); + selectors.sort_unstable(); + + ExpandData { + name: format_ident!("{contract_name}Events"), + variants, + types: None, + min_data_len: events + .iter() + .map(|event| ty::params_base_data_size(cx, &event.params())) + .min() + .unwrap(), + trait_: format_ident!("SolEvent"), + selectors, + } + } } } +} - fn expand(self, attrs: Vec, extra_methods: bool) -> TokenStream { - let Self { name, variants, min_data_len, trait_, .. } = &self; - let types = self.types(); - - assert_eq!(variants.len(), types.len()); +impl<'a> CallLikeExpander<'a> { + fn expand(&self, to_expand: ToExpand<'_>, attrs: Vec) -> TokenStream { + let data @ ExpandData { name, variants, min_data_len, trait_, .. } = + &to_expand.to_data(self); + let types = data.types(); let name_s = name.to_string(); let count = variants.len(); - let def = self.generate_enum(attrs, extra_methods); + let def = self.generate_enum(data, attrs); + + // TODO: SolInterface for events + if matches!(to_expand, ToExpand::Events(_)) { + return def; + } + quote! { #def @@ -317,26 +432,14 @@ impl<'a> CallLikeExpander<'a> { } } - fn expand_event(self, attrs: Vec, extra_methods: bool) -> TokenStream { - // TODO: SolInterface for events - self.generate_enum(attrs, extra_methods) - } - - fn generate_enum(&self, mut attrs: Vec, extra_methods: bool) -> TokenStream { - let Self { name, variants, data, .. } = self; - let (selectors, selector_type) = match data { - CallLikeExpanderData::Function { selectors, .. } - | CallLikeExpanderData::Error { selectors } => { - (quote!(#(#selectors,)*), quote!([u8; 4])) - } - CallLikeExpanderData::Event { selectors } => { - (quote!(#(#selectors,)*), quote!([u8; 32])) - } - }; - - let types = self.types(); + fn generate_enum(&self, data: &ExpandData, mut attrs: Vec) -> TokenStream { + let ExpandData { name, variants, selectors, .. } = data; + let types = data.types(); + let selector_len = selectors.first().unwrap().array.len(); + assert!(selectors.iter().all(|s| s.array.len() == selector_len)); + let selector_type = quote!([u8; #selector_len]); self.cx.type_derives(&mut attrs, types.iter().cloned().map(ast::Type::custom), false); - let tokens = quote! { + let mut tokens = quote! { #(#attrs)* pub enum #name { #(#variants(#types),)* @@ -348,27 +451,24 @@ impl<'a> CallLikeExpander<'a> { /// /// Note that the selectors might not be in the same order as the /// variants, as they are sorted instead of ordered by definition. - pub const SELECTORS: &'static [#selector_type] = &[#selectors]; + pub const SELECTORS: &'static [#selector_type] = &[#(#selectors),*]; } }; - if extra_methods { + if self.extra_methods { let conversions = variants.iter().zip(types).map(|(v, t)| generate_variant_conversions(name, v, t)); let methods = variants.iter().zip(types).map(generate_variant_methods); - quote! { - #tokens - - #(#conversions)* - + tokens.extend(conversions); + tokens.extend(quote! { #[automatically_derived] impl #name { #(#methods)* } - } - } else { - tokens + }); } + + tokens } } diff --git a/crates/sol-macro/src/expand/error.rs b/crates/sol-macro/src/expand/error.rs index e93e1f41d..12f3d17bb 100644 --- a/crates/sol-macro/src/expand/error.rs +++ b/crates/sol-macro/src/expand/error.rs @@ -25,6 +25,7 @@ pub(super) fn expand(cx: &ExpCtxt<'_>, error: &ItemError) -> Result let (sol_attrs, mut attrs) = crate::attr::SolAttrs::parse(attrs)?; cx.derives(&mut attrs, params, true); let docs = sol_attrs.docs.or(cx.attrs.docs).unwrap_or(true); + let abi = sol_attrs.abi.or(cx.attrs.abi).unwrap_or(false); let tokenize_impl = expand_tokenize(params); @@ -34,12 +35,28 @@ pub(super) fn expand(cx: &ExpCtxt<'_>, error: &ItemError) -> Result let converts = expand_from_into_tuples(&name.0, params); let fields = expand_fields(params); let doc = docs.then(|| { - let selector = hex::encode_prefixed(selector.array); + let selector = hex::encode_prefixed(selector.array.as_slice()); attr::mk_doc(format!( "Custom error with signature `{signature}` and selector `{selector}`.\n\ ```solidity\n{error}\n```" )) }); + let abi: Option = abi.then(|| { + if_json! { + let error = super::to_abi::generate(error, cx); + quote! { + #[automatically_derived] + impl ::alloy_sol_types::JsonAbiExt for #name { + type Abi = ::alloy_sol_types::private::alloy_json_abi::Error; + + #[inline] + fn abi() -> Self::Abi { + #error + } + } + } + } + }); let tokens = quote! { #(#attrs)* #doc @@ -71,6 +88,8 @@ pub(super) fn expand(cx: &ExpCtxt<'_>, error: &ItemError) -> Result #tokenize_impl } } + + #abi }; }; Ok(tokens) diff --git a/crates/sol-macro/src/expand/event.rs b/crates/sol-macro/src/expand/event.rs index b7853be45..c5bd365f4 100644 --- a/crates/sol-macro/src/expand/event.rs +++ b/crates/sol-macro/src/expand/event.rs @@ -25,6 +25,7 @@ pub(super) fn expand(cx: &ExpCtxt<'_>, event: &ItemEvent) -> Result let (sol_attrs, mut attrs) = crate::attr::SolAttrs::parse(attrs)?; cx.derives(&mut attrs, ¶ms, true); let docs = sol_attrs.docs.or(cx.attrs.docs).unwrap_or(true); + let abi = sol_attrs.abi.or(cx.attrs.abi).unwrap_or(false); cx.assert_resolved(¶ms)?; event.assert_valid()?; @@ -101,12 +102,30 @@ pub(super) fn expand(cx: &ExpCtxt<'_>, event: &ItemEvent) -> Result .map(|(i, assign)| quote!(out[#i] = #assign;)); let doc = docs.then(|| { - let selector = hex::encode_prefixed(selector.array); + let selector = hex::encode_prefixed(selector.array.as_slice()); attr::mk_doc(format!( "Event with signature `{signature}` and selector `{selector}`.\n\ ```solidity\n{event}\n```" )) }); + + let abi: Option = abi.then(|| { + if_json! { + let event = super::to_abi::generate(event, cx); + quote! { + #[automatically_derived] + impl ::alloy_sol_types::JsonAbiExt for #name { + type Abi = ::alloy_sol_types::private::alloy_json_abi::Event; + + #[inline] + fn abi() -> Self::Abi { + #event + } + } + } + } + }); + let tokens = quote! { #(#attrs)* #doc @@ -163,6 +182,8 @@ pub(super) fn expand(cx: &ExpCtxt<'_>, event: &ItemEvent) -> Result Ok(()) } } + + #abi }; }; Ok(tokens) diff --git a/crates/sol-macro/src/expand/function.rs b/crates/sol-macro/src/expand/function.rs index d389d5ab5..4d6d1db29 100644 --- a/crates/sol-macro/src/expand/function.rs +++ b/crates/sol-macro/src/expand/function.rs @@ -24,43 +24,44 @@ use syn::Result; /// } /// ``` pub(super) fn expand(cx: &ExpCtxt<'_>, function: &ItemFunction) -> Result { - let ItemFunction { attrs, arguments, returns, name: Some(_), .. } = function else { + let ItemFunction { attrs, parameters, returns, name: Some(_), .. } = function else { // ignore functions without names (constructors, modifiers...) return Ok(quote!()); }; let returns = returns.as_ref().map(|r| &r.returns).unwrap_or_default(); - cx.assert_resolved(arguments)?; + cx.assert_resolved(parameters)?; if !returns.is_empty() { cx.assert_resolved(returns)?; } let (sol_attrs, mut call_attrs) = crate::attr::SolAttrs::parse(attrs)?; let mut return_attrs = call_attrs.clone(); - cx.derives(&mut call_attrs, arguments, true); + cx.derives(&mut call_attrs, parameters, true); if !returns.is_empty() { cx.derives(&mut return_attrs, returns, true); } let docs = sol_attrs.docs.or(cx.attrs.docs).unwrap_or(true); + let abi = sol_attrs.abi.or(cx.attrs.abi).unwrap_or(false); let call_name = cx.call_name(function); let return_name = cx.return_name(function); - let call_fields = expand_fields(arguments); + let call_fields = expand_fields(parameters); let return_fields = expand_fields(returns); - let call_tuple = expand_tuple_types(arguments.types()).0; + let call_tuple = expand_tuple_types(parameters.types()).0; let return_tuple = expand_tuple_types(returns.types()).0; - let converts = expand_from_into_tuples(&call_name, arguments); + let converts = expand_from_into_tuples(&call_name, parameters); let return_converts = expand_from_into_tuples(&return_name, returns); let signature = cx.function_signature(function); let selector = crate::utils::selector(&signature); - let tokenize_impl = expand_tokenize(arguments); + let tokenize_impl = expand_tokenize(parameters); let call_doc = docs.then(|| { - let selector = hex::encode_prefixed(selector.array); + let selector = hex::encode_prefixed(selector.array.as_slice()); attr::mk_doc(format!( "Function with signature `{signature}` and selector `{selector}`.\n\ ```solidity\n{function}\n```" @@ -72,6 +73,23 @@ pub(super) fn expand(cx: &ExpCtxt<'_>, function: &ItemFunction) -> Result = abi.then(|| { + if_json! { + let function = super::to_abi::generate(function, cx); + quote! { + #[automatically_derived] + impl ::alloy_sol_types::JsonAbiExt for #call_name { + type Abi = ::alloy_sol_types::private::alloy_json_abi::Function; + + #[inline] + fn abi() -> Self::Abi { + #function + } + } + } + } + }); + let tokens = quote! { #(#call_attrs)* #call_doc @@ -119,6 +137,8 @@ pub(super) fn expand(cx: &ExpCtxt<'_>, function: &ItemFunction) -> Result as ::alloy_sol_types::SolType>::abi_decode_sequence(data, validate).map(Into::into) } } + + #abi }; }; Ok(tokens) diff --git a/crates/sol-macro/src/expand/macros.rs b/crates/sol-macro/src/expand/macros.rs new file mode 100644 index 000000000..c43758c97 --- /dev/null +++ b/crates/sol-macro/src/expand/macros.rs @@ -0,0 +1,12 @@ +#[cfg(feature = "json")] +macro_rules! if_json { + ($($t:tt)*) => { $($t)* }; +} + +#[cfg(not(feature = "json"))] +macro_rules! if_json { + ($($t:tt)*) => { + crate::expand::emit_json_error(); + TokenStream::new() + }; +} diff --git a/crates/sol-macro/src/expand/mod.rs b/crates/sol-macro/src/expand/mod.rs index 10cedb4d5..59d99218f 100644 --- a/crates/sol-macro/src/expand/mod.rs +++ b/crates/sol-macro/src/expand/mod.rs @@ -12,9 +12,16 @@ use ast::{ use indexmap::IndexMap; use proc_macro2::{Delimiter, Group, Ident, Punct, Spacing, Span, TokenStream, TokenTree}; use quote::{format_ident, quote, TokenStreamExt}; -use std::{borrow::Borrow, fmt::Write}; +use std::{ + borrow::Borrow, + fmt::Write, + sync::atomic::{AtomicBool, Ordering}, +}; use syn::{ext::IdentExt, parse_quote, Attribute, Error, Result}; +#[macro_use] +mod macros; + mod ty; pub use ty::expand_type; @@ -27,6 +34,9 @@ mod r#struct; mod udt; mod var_def; +#[cfg(feature = "json")] +mod to_abi; + /// The limit for the number of times to resolve a type. const RESOLVE_LIMIT: usize = 32; @@ -278,7 +288,7 @@ impl<'a> OverloadedItem<'a> { fn eq_by_types(self, other: Self) -> bool { match (self, other) { - (Self::Function(a), Self::Function(b)) => a.arguments.types().eq(b.arguments.types()), + (Self::Function(a), Self::Function(b)) => a.parameters.types().eq(b.parameters.types()), (Self::Event(a), Self::Event(b)) => a.param_types().eq(b.param_types()), _ => false, } @@ -314,6 +324,18 @@ impl<'ast> ExpCtxt<'ast> { self.all_items.iter().copied().find(|item| item.name() == Some(name)) } + /// Recursively resolves the given type by constructing a new one. + #[allow(dead_code)] + fn make_resolved_type(&self, ty: &Type) -> Type { + let mut ty = ty.clone(); + ty.visit_mut(|ty| { + if let Type::Custom(name) = ty { + *ty = self.custom_type(name).clone(); + } + }); + ty + } + fn custom_type(&self, name: &SolPath) -> &Type { match self.try_custom_type(name) { Some(item) => item, @@ -369,27 +391,27 @@ impl<'ast> ExpCtxt<'ast> { } fn function_signature(&self, function: &ItemFunction) -> String { - self.signature(function.name().as_string(), &function.arguments) + self.signature(function.name().as_string(), &function.parameters) } - fn function_selector(&self, function: &ItemFunction) -> ExprArray { - utils::selector(self.function_signature(function)) + fn function_selector(&self, function: &ItemFunction) -> ExprArray { + utils::selector(self.function_signature(function)).with_span(function.span()) } fn error_signature(&self, error: &ItemError) -> String { self.signature(error.name.as_string(), &error.parameters) } - fn error_selector(&self, error: &ItemError) -> ExprArray { - utils::selector(self.error_signature(error)) + fn error_selector(&self, error: &ItemError) -> ExprArray { + utils::selector(self.error_signature(error)).with_span(error.span()) } fn event_signature(&self, event: &ItemEvent) -> String { self.signature(event.name.as_string(), &event.params()) } - fn event_selector(&self, event: &ItemEvent) -> ExprArray { - utils::event_selector(self.event_signature(event)) + fn event_selector(&self, event: &ItemEvent) -> ExprArray { + utils::event_selector(self.event_signature(event)).with_span(event.span()) } /// Formats the name and parameters of the function as a Solidity signature. @@ -605,3 +627,14 @@ fn tokenize_<'a>( (#(#statements,)*) } } + +#[allow(dead_code)] +fn emit_json_error() { + static EMITTED: AtomicBool = AtomicBool::new(false); + if !EMITTED.swap(true, Ordering::Relaxed) { + emit_error!( + Span::call_site(), + "the `#[sol(dyn_abi)]` attribute requires the `\"json\"` feature" + ); + } +} diff --git a/crates/sol-macro/src/expand/to_abi.rs b/crates/sol-macro/src/expand/to_abi.rs new file mode 100644 index 000000000..620314159 --- /dev/null +++ b/crates/sol-macro/src/expand/to_abi.rs @@ -0,0 +1,177 @@ +use super::ExpCtxt; +use crate::verbatim::Verbatim; +use alloy_json_abi::{ + Constructor, Error, Event, EventParam, Fallback, Function, Param, Receive, StateMutability, +}; +use ast::{ItemError, ItemEvent, ItemFunction}; +use proc_macro2::TokenStream; + +pub fn generate(t: &T, cx: &ExpCtxt<'_>) -> TokenStream +where + T: ToAbi, + T::DynAbi: Verbatim, +{ + crate::verbatim::verbatim(&t.to_dyn_abi(cx)) +} + +pub trait ToAbi { + type DynAbi; + + fn to_dyn_abi(&self, cx: &ExpCtxt<'_>) -> Self::DynAbi; +} + +impl ToAbi for ast::ItemFunction { + type DynAbi = Function; + + fn to_dyn_abi(&self, cx: &ExpCtxt<'_>) -> Self::DynAbi { + Function { + name: self.name.as_ref().map(|i| i.as_string()).unwrap_or_default(), + inputs: self.parameters.to_dyn_abi(cx), + outputs: self.returns.as_ref().map(|r| r.returns.to_dyn_abi(cx)).unwrap_or_default(), + state_mutability: self.attributes.to_dyn_abi(cx), + } + } +} + +impl ToAbi for ast::ItemError { + type DynAbi = Error; + + fn to_dyn_abi(&self, cx: &ExpCtxt<'_>) -> Self::DynAbi { + Error { name: self.name.as_string(), inputs: self.parameters.to_dyn_abi(cx) } + } +} + +impl ToAbi for ast::ItemEvent { + type DynAbi = Event; + + fn to_dyn_abi(&self, cx: &ExpCtxt<'_>) -> Self::DynAbi { + Event { + name: self.name.as_string(), + inputs: self.parameters.iter().map(|e| e.to_dyn_abi(cx)).collect(), + anonymous: self.is_anonymous(), + } + } +} + +impl

ToAbi for ast::Parameters

{ + type DynAbi = Vec; + + fn to_dyn_abi(&self, cx: &ExpCtxt<'_>) -> Self::DynAbi { + self.iter().map(|p| p.to_dyn_abi(cx)).collect() + } +} + +impl ToAbi for ast::VariableDeclaration { + type DynAbi = Param; + + fn to_dyn_abi(&self, cx: &ExpCtxt<'_>) -> Self::DynAbi { + ty_to_param(self.name.as_ref().map(ast::SolIdent::as_string), &self.ty, cx) + } +} + +impl ToAbi for ast::EventParameter { + type DynAbi = EventParam; + + fn to_dyn_abi(&self, cx: &ExpCtxt<'_>) -> Self::DynAbi { + let name = self.name.as_ref().map(ast::SolIdent::as_string); + let Param { ty, name, components, internal_type } = ty_to_param(name, &self.ty, cx); + EventParam { ty, name, indexed: self.is_indexed(), internal_type, components } + } +} + +impl ToAbi for ast::FunctionAttributes { + type DynAbi = StateMutability; + + fn to_dyn_abi(&self, _cx: &ExpCtxt<'_>) -> Self::DynAbi { + match self.mutability() { + Some(ast::Mutability::Pure(_) | ast::Mutability::Constant(_)) => StateMutability::Pure, + Some(ast::Mutability::View(_)) => StateMutability::View, + Some(ast::Mutability::Payable(_)) => StateMutability::Payable, + None => StateMutability::NonPayable, + } + } +} + +fn ty_to_param(name: Option, ty: &ast::Type, cx: &ExpCtxt<'_>) -> Param { + let mut ty_name = ty.abi_name(); + + // HACK: `cx.custom_type` resolves the custom type recursively, so in recursive structs the + // peeled `ty` will be `Tuple` rather than `Custom`. + if ty_name.starts_with('(') { + let paren_i = ty_name.rfind(')').expect("malformed tuple type"); + let suffix = &ty_name[paren_i + 1..]; + ty_name = format!("tuple{suffix}"); + } + + let mut component_names = vec![]; + let resolved = match ty.peel_arrays() { + ast::Type::Custom(name) => { + if let ast::Item::Struct(s) = cx.item(name) { + component_names = s + .fields + .names() + .map(|n| n.map(|i| i.as_string()).unwrap_or_default()) + .collect(); + } + cx.custom_type(name) + } + ty => ty, + }; + + let components = if let ast::Type::Tuple(tuple) = resolved { + tuple + .types + .iter() + .enumerate() + .map(|(i, ty)| ty_to_param(component_names.get(i).cloned(), ty, cx)) + .collect() + } else { + vec![] + }; + + // TODO: internal_type + let internal_type = None; + + Param { ty: ty_name, name: name.unwrap_or_default(), internal_type, components } +} + +pub(super) fn constructor(function: &ItemFunction, cx: &ExpCtxt<'_>) -> Constructor { + assert!(function.kind.is_constructor()); + Constructor { + inputs: function.parameters.to_dyn_abi(cx), + state_mutability: function.attributes.to_dyn_abi(cx), + } +} + +pub(super) fn fallback(function: &ItemFunction, _cx: &ExpCtxt<'_>) -> Fallback { + assert!(function.kind.is_fallback()); + Fallback { state_mutability: StateMutability::NonPayable } +} + +pub(super) fn receive(function: &ItemFunction, _cx: &ExpCtxt<'_>) -> Receive { + assert!(function.kind.is_receive()); + Receive { state_mutability: StateMutability::Payable } +} + +macro_rules! make_map { + ($items:ident, $cx:ident) => {{ + let mut map = std::collections::BTreeMap::>::new(); + for item in $items { + let item = item.to_dyn_abi($cx); + map.entry(item.name.clone()).or_default().push(item); + } + crate::verbatim::verbatim(&map) + }}; +} + +pub(super) fn functions_map(functions: &[&ItemFunction], cx: &ExpCtxt<'_>) -> TokenStream { + make_map!(functions, cx) +} + +pub(super) fn events_map(events: &[&ItemEvent], cx: &ExpCtxt<'_>) -> TokenStream { + make_map!(events, cx) +} + +pub(super) fn errors_map(errors: &[&ItemError], cx: &ExpCtxt<'_>) -> TokenStream { + make_map!(errors, cx) +} diff --git a/crates/sol-macro/src/json.rs b/crates/sol-macro/src/json.rs index 542a6bc69..086204c07 100644 --- a/crates/sol-macro/src/json.rs +++ b/crates/sol-macro/src/json.rs @@ -141,7 +141,7 @@ mod tests { ast::Visibility::External(Default::default()) ))); - let args = &f.arguments; + let args = &f.parameters; assert_eq!(args.len(), 7); assert_eq!(args[0].ty.to_string(), "AdvancedOrder[]"); @@ -187,7 +187,7 @@ mod tests { ]; for (f, name, ty) in function_tests { assert_eq!(f.name.as_ref().unwrap(), name); - assert_eq!(f.arguments.type_strings().collect::>(), [ty]); + assert_eq!(f.parameters.type_strings().collect::>(), [ty]); let ret = &f.returns.as_ref().expect("no returns").returns; assert_eq!(ret.type_strings().collect::>(), [ty]); } diff --git a/crates/sol-macro/src/lib.rs b/crates/sol-macro/src/lib.rs index f8be03b57..dbc2d8768 100644 --- a/crates/sol-macro/src/lib.rs +++ b/crates/sol-macro/src/lib.rs @@ -26,9 +26,13 @@ use syn::parse_macro_input; mod attr; mod expand; mod input; +mod utils; + +#[cfg(feature = "json")] +mod verbatim; + #[cfg(feature = "json")] mod json; -mod utils; /// Generate types that implement [`alloy-sol-types`] traits, which can be used /// for type-safe [ABI] and [EIP-712] serialization to interface with Ethereum @@ -96,6 +100,19 @@ mod utils; /// [`abigen`][abigen] /// - `docs [ = ]`: adds doc comments to all generated types. This is the default /// behaviour of [`abigen`][abigen] +/// - `abi [ = ]`: generates functions which return the dynamic ABI representation +/// (provided by [`alloy_json_abi`](https://docs.rs/alloy-json-abi)) of all the generated items. +/// Requires the `"json"` feature. For: +/// - contracts: generates an `abi` module nested inside of the contract module, which contains: +/// - `pub fn contract() -> JsonAbi`, +/// - `pub fn constructor() -> Option` +/// - `pub fn fallback() -> Option` +/// - `pub fn receive() -> Option` +/// - `pub fn functions() -> BTreeMap>` +/// - `pub fn events() -> BTreeMap>` +/// - `pub fn errors() -> BTreeMap>` +/// - items: generates implementations of the `SolAbiExt` trait, alongside the existing +/// [`alloy-sol-types`] traits /// - `bytecode = `: specifies the creation/init bytecode of a contract. This /// will emit a `static` item with the specified bytes. /// - `deployed_bytecode = `: specifies the deployed bytecode of a contract. diff --git a/crates/sol-macro/src/utils.rs b/crates/sol-macro/src/utils.rs index 2e32224f7..a5cc8e2ee 100644 --- a/crates/sol-macro/src/utils.rs +++ b/crates/sol-macro/src/utils.rs @@ -1,3 +1,4 @@ +use ast::Spanned; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use tiny_keccak::{Hasher, Keccak}; @@ -13,12 +14,12 @@ pub fn keccak256>(bytes: T) -> [u8; 32] { output } -pub fn selector>(bytes: T) -> ExprArray { - ExprArray::new(keccak256(bytes)[..4].try_into().unwrap()) +pub fn selector>(bytes: T) -> ExprArray { + ExprArray::new(keccak256(bytes)[..4].to_vec()) } -pub fn event_selector>(bytes: T) -> ExprArray { - ExprArray::new(keccak256(bytes)) +pub fn event_selector>(bytes: T) -> ExprArray { + ExprArray::new(keccak256(bytes).to_vec()) } pub fn combine_errors(v: impl IntoIterator) -> syn::Result<()> { @@ -31,18 +32,48 @@ pub fn combine_errors(v: impl IntoIterator) -> syn::Result<() } } -pub struct ExprArray { - pub array: [T; N], +pub struct ExprArray { + pub array: Vec, pub span: Span, } -impl ExprArray { - fn new(array: [T; N]) -> Self { +impl PartialOrd for ExprArray { + fn partial_cmp(&self, other: &Self) -> Option { + self.array.partial_cmp(&other.array) + } +} + +impl Ord for ExprArray { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.array.cmp(&other.array) + } +} + +impl PartialEq for ExprArray { + fn eq(&self, other: &Self) -> bool { + self.array == other.array + } +} + +impl Eq for ExprArray {} + +impl Spanned for ExprArray { + fn span(&self) -> Span { + self.span + } + + fn set_span(&mut self, span: Span) { + self.span = span; + } +} + +impl ExprArray { + fn new(array: Vec) -> Self { Self { array, span: Span::call_site() } } } -impl ToTokens for ExprArray { +impl ToTokens for ExprArray { fn to_tokens(&self, tokens: &mut TokenStream) { syn::token::Bracket(self.span).surround(tokens, |tokens| { for t in &self.array { diff --git a/crates/sol-macro/src/verbatim.rs b/crates/sol-macro/src/verbatim.rs new file mode 100644 index 000000000..cda085d63 --- /dev/null +++ b/crates/sol-macro/src/verbatim.rs @@ -0,0 +1,164 @@ +use std::collections::BTreeMap; + +use proc_macro2::TokenStream; +use quote::quote; + +pub fn verbatim(t: &T) -> TokenStream { + let mut s = TokenStream::new(); + t.to_tokens(&mut s); + s +} + +pub trait Verbatim { + fn to_tokens(&self, s: &mut TokenStream); + + #[inline] + fn verbatim(&self) -> ToTokensCompat<'_, Self> { + ToTokensCompat(self) + } + + #[inline] + fn into_verbatim(self) -> IntoTokensCompat + where + Self: Sized, + { + IntoTokensCompat(self) + } +} + +pub struct ToTokensCompat<'a, T: ?Sized + Verbatim>(pub &'a T); + +impl quote::ToTokens for ToTokensCompat<'_, T> { + #[inline] + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens) + } +} + +pub struct IntoTokensCompat(pub T); + +impl quote::ToTokens for IntoTokensCompat { + #[inline] + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens) + } +} + +impl Verbatim for String { + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.extend(if self.is_empty() { + quote!(::alloy_sol_types::private::String::new()) + } else { + quote!(::alloy_sol_types::private::ToOwned::to_owned(#self)) + }) + } +} + +impl Verbatim for bool { + #[inline] + fn to_tokens(&self, s: &mut TokenStream) { + quote::ToTokens::to_tokens(self, s) + } +} + +impl Verbatim for usize { + #[inline] + fn to_tokens(&self, s: &mut TokenStream) { + quote::ToTokens::to_tokens(self, s) + } +} + +impl Verbatim for Vec { + fn to_tokens(&self, s: &mut TokenStream) { + let iter = self.iter().map(ToTokensCompat); + s.extend(quote!(::alloy_sol_types::private::vec![#(#iter),*])); + } +} + +impl Verbatim for BTreeMap { + fn to_tokens(&self, s: &mut TokenStream) { + let k = self.keys().map(ToTokensCompat); + let v = self.values().map(ToTokensCompat); + s.extend(quote!(::alloy_sol_types::private::BTreeMap::from([#( (#k, #v) ),*]))); + } +} + +impl Verbatim for Option { + fn to_tokens(&self, s: &mut TokenStream) { + let tts = match self { + Some(t) => { + let mut s = TokenStream::new(); + t.to_tokens(&mut s); + quote!(::core::option::Option::Some(#s)) + } + None => quote!(::core::option::Option::None), + }; + s.extend(tts); + } +} + +macro_rules! derive_verbatim { + () => {}; + + (struct $name:ident { $($field:ident),* $(,)? } $($rest:tt)*) => { + impl Verbatim for alloy_json_abi::$name { + fn to_tokens(&self, s: &mut TokenStream) { + let Self { $($field),* } = self; + $( + let $field = ToTokensCompat($field); + )* + s.extend(quote! { + ::alloy_sol_types::private::alloy_json_abi::$name { + $($field: #$field,)* + } + }); + } + } + derive_verbatim!($($rest)*); + }; + + (enum $name:ident { $($variant:ident $( { $($field_idx:tt : $field:ident),* $(,)? } )?),* $(,)? } $($rest:tt)*) => { + impl Verbatim for alloy_json_abi::$name { + fn to_tokens(&self, s: &mut TokenStream) { + match self {$( + Self::$variant $( { $($field_idx: $field),* } )? => { + $($( + let $field = ToTokensCompat($field); + )*)? + s.extend(quote! { + ::alloy_sol_types::private::alloy_json_abi::$name::$variant $( { $($field_idx: #$field),* } )? + }); + } + )*} + } + } + derive_verbatim!($($rest)*); + }; +} + +derive_verbatim! { + // struct JsonAbi { constructor, functions, events, errors, receive, fallback } + struct Constructor { inputs, state_mutability } + struct Fallback { state_mutability } + struct Receive { state_mutability } + struct Function { name, inputs, outputs, state_mutability } + struct Error { name, inputs } + struct Event { name, inputs, anonymous } + struct Param { ty, name, components, internal_type } + struct EventParam { ty, name, indexed, components, internal_type } + + enum InternalType { + AddressPayable { 0: s }, + Contract { 0: s }, + Enum { contract: contract, ty: ty }, + Struct { contract: contract, ty: ty }, + Other { contract: contract, ty: ty }, + } + + enum StateMutability { + Pure, + View, + NonPayable, + Payable, + } +} diff --git a/crates/sol-types/Cargo.toml b/crates/sol-types/Cargo.toml index f3c5787a4..b84b4a6e6 100644 --- a/crates/sol-types/Cargo.toml +++ b/crates/sol-types/Cargo.toml @@ -23,13 +23,18 @@ alloy-sol-macro.workspace = true hex.workspace = true +# json +alloy-json-abi = { workspace = true, optional = true } + +# eip712-serde serde = { workspace = true, optional = true, features = ["derive"] } [dev-dependencies] alloy-primitives = { workspace = true, features = ["arbitrary", "serde"] } paste.workspace = true +pretty_assertions.workspace = true serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } +serde_json.workspace = true proptest.workspace = true rustversion = "1.0" @@ -38,6 +43,6 @@ trybuild = "1.0" [features] default = ["std"] std = ["alloy-primitives/std", "hex/std", "serde?/std"] -json = ["alloy-sol-macro/json"] +json = ["dep:alloy-json-abi", "alloy-sol-macro/json"] eip712-serde = ["dep:serde", "alloy-primitives/serde"] arbitrary = ["alloy-primitives/arbitrary"] diff --git a/crates/sol-types/src/ext.rs b/crates/sol-types/src/ext.rs new file mode 100644 index 000000000..5b1549ded --- /dev/null +++ b/crates/sol-types/src/ext.rs @@ -0,0 +1,14 @@ +/// Extension trait for ABI representation. +/// +/// Implemented by types generated by the [`sol!`] macro when using the [`#[sol(abi)]`][attr] +/// attribute. +/// +/// [`sol!`]: crate::sol +/// [attr]: https://docs.rs/alloy-sol-macro/latest/alloy_sol_macro/macro.sol.html#attributes +pub trait JsonAbiExt { + /// The ABI representation of this type. + type Abi; + + /// Returns the ABI representation of this type. + fn abi() -> Self::Abi; +} diff --git a/crates/sol-types/src/lib.rs b/crates/sol-types/src/lib.rs index 4669041da..f590ff345 100644 --- a/crates/sol-types/src/lib.rs +++ b/crates/sol-types/src/lib.rs @@ -173,6 +173,11 @@ pub mod abi; mod errors; pub use errors::{Error, Result}; +#[cfg(feature = "json")] +mod ext; +#[cfg(feature = "json")] +pub use ext::JsonAbiExt; + mod impl_core; mod types; @@ -198,18 +203,29 @@ pub use alloy_sol_macro::sol; pub mod private { pub use super::utils::{just_ok, next_multiple_of_32, words_for, words_for_len}; pub use alloc::{ - borrow::{Borrow, Cow, ToOwned}, + borrow::{Cow, ToOwned}, + collections::BTreeMap, string::{String, ToString}, + vec, vec::Vec, }; pub use alloy_primitives::{ bytes, keccak256, Address, Bytes, FixedBytes, Function, Signed, Uint, B256, I256, U256, }; - pub use core::{convert::From, default::Default, option::Option, result::Result}; + pub use core::{ + borrow::{Borrow, BorrowMut}, + convert::From, + default::Default, + option::Option, + result::Result, + }; pub use Option::{None, Some}; pub use Result::{Err, Ok}; + #[cfg(feature = "json")] + pub use alloy_json_abi; + /// An ABI-encodable is any type that may be encoded via a given `SolType`. /// /// The `SolType` trait contains encoding logic for a single associated diff --git a/crates/sol-types/tests/compiletest.rs b/crates/sol-types/tests/compiletest.rs index 56475c58f..fc9e85dc0 100644 --- a/crates/sol-types/tests/compiletest.rs +++ b/crates/sol-types/tests/compiletest.rs @@ -8,9 +8,9 @@ fn ui() { macro_rules! feature_tests { ($($f:literal),* $(,)?) => {$( #[cfg(feature = $f)] - t.compile_fail(concat!("tests/ui/feature/", $f, "/*.rs")); + t.compile_fail(concat!("tests/ui/features/", $f, "/*.rs")); #[cfg(not(feature = $f))] - t.compile_fail(concat!("tests/ui/feature/not(", $f, ")/*.rs")); + t.compile_fail(concat!("tests/ui/features/not(", $f, ")/*.rs")); )*}; } diff --git a/crates/sol-types/tests/macros/sol/abi.rs b/crates/sol-types/tests/macros/sol/abi.rs new file mode 100644 index 000000000..69f5086db --- /dev/null +++ b/crates/sol-types/tests/macros/sol/abi.rs @@ -0,0 +1,454 @@ +use alloy_json_abi::{ + Constructor, Error, EventParam, Fallback, Function, Param, Receive, StateMutability, +}; +use alloy_sol_types::{sol, JsonAbiExt}; +use pretty_assertions::assert_eq; +use std::collections::BTreeMap; + +macro_rules! abi_map { + ($($k:expr => $v:expr),* $(,)?) => { + BTreeMap::from([$(($k.into(), vec![$v])),*]) + }; +} + +#[test] +fn equal_abis() { + let contract = Contract::abi::contract(); + + assert_eq!(contract.constructor, Contract::abi::constructor()); + assert_eq!( + contract.constructor, + Some(Constructor { inputs: Vec::new(), state_mutability: StateMutability::NonPayable }) + ); + + assert_eq!(contract.fallback, Contract::abi::fallback()); + assert_eq!(contract.fallback, Some(Fallback { state_mutability: StateMutability::NonPayable })); + + assert_eq!(contract.receive, Contract::abi::receive()); + assert_eq!(contract.receive, Some(Receive { state_mutability: StateMutability::Payable })); + + assert_eq!(contract.functions, Contract::abi::functions()); + assert_eq!( + *contract.function("F00").unwrap().first().unwrap(), + Function { + name: "F00".into(), + inputs: vec![], + outputs: vec![], + state_mutability: StateMutability::NonPayable, + } + ); + assert_eq!( + *contract.function("F01").unwrap().first().unwrap(), + Function { + name: "F01".into(), + inputs: vec![param("uint a")], + outputs: vec![], + state_mutability: StateMutability::Payable, + } + ); + assert_eq!( + *contract.function("F02").unwrap().first().unwrap(), + Function { + name: "F02".into(), + inputs: vec![param("uint "), param("bool b")], + outputs: vec![], + state_mutability: StateMutability::View, + } + ); + assert_eq!( + *contract.function("F10").unwrap().first().unwrap(), + Function { + name: "F10".into(), + inputs: vec![], + outputs: vec![], + state_mutability: StateMutability::Pure, + } + ); + assert_eq!( + *contract.function("F11").unwrap().first().unwrap(), + Function { + name: "F11".into(), + inputs: vec![param("uint a")], + outputs: vec![param("uint a")], + state_mutability: StateMutability::NonPayable, + } + ); + assert_eq!( + *contract.function("F12").unwrap().first().unwrap(), + Function { + name: "F12".into(), + inputs: vec![param("uint "), param("bool b")], + outputs: vec![param("uint "), param("bool b")], + state_mutability: StateMutability::NonPayable, + } + ); + assert_eq!( + *contract.function("F20").unwrap().first().unwrap(), + Function { + name: "F20".into(), + inputs: vec![param("uint "), param("uint[] "), param("uint[][1] ")], + outputs: vec![], + state_mutability: StateMutability::NonPayable, + } + ); + assert_eq!( + *contract.function("F21").unwrap().first().unwrap(), + Function { + name: "F21".into(), + inputs: vec![ + Param { + ty: "tuple".into(), + name: String::new(), + components: vec![param("uint custom")], + internal_type: None, + }, + Param { + ty: "tuple[]".into(), + name: String::new(), + components: vec![param("uint custom")], + internal_type: None, + }, + Param { + ty: "tuple[][2]".into(), + name: String::new(), + components: vec![param("uint custom")], + internal_type: None, + }, + ], + outputs: vec![], + state_mutability: StateMutability::NonPayable, + } + ); + let custom = Param { + ty: "tuple".into(), + name: "cs".into(), + // TODO: should be `uint custom`, but name is lost in recursive resolution + components: vec![param("uint ")], + internal_type: None, + }; + assert_eq!( + *contract.function("F22").unwrap().first().unwrap(), + Function { + name: "F22".into(), + inputs: vec![ + Param { + ty: "tuple".into(), + name: String::new(), + components: vec![custom.clone(), param("bool cb")], + internal_type: None, + }, + Param { + ty: "tuple[]".into(), + name: String::new(), + components: vec![custom.clone(), param("bool cb")], + internal_type: None, + }, + Param { + ty: "tuple[][3]".into(), + name: String::new(), + components: vec![custom, param("bool cb")], + internal_type: None, + }, + ], + outputs: vec![], + state_mutability: StateMutability::NonPayable, + } + ); + assert_eq!( + contract.functions, + abi_map! { + "F00" => Contract::F00Call::abi(), + "F01" => Contract::F01Call::abi(), + "F02" => Contract::F02Call::abi(), + "F10" => Contract::F10Call::abi(), + "F11" => Contract::F11Call::abi(), + "F12" => Contract::F12Call::abi(), + "F20" => Contract::F20Call::abi(), + "F21" => Contract::F21Call::abi(), + "F22" => Contract::F22Call::abi(), + } + ); + + assert_eq!(contract.events, Contract::abi::events()); + assert_eq!( + *contract.event("EV00").unwrap().first().unwrap(), + alloy_json_abi::Event { name: "EV00".into(), inputs: vec![], anonymous: false } + ); + assert_eq!( + *contract.event("EV01").unwrap().first().unwrap(), + alloy_json_abi::Event { + name: "EV01".into(), + inputs: vec![eparam("uint a", false)], + anonymous: false, + } + ); + assert_eq!( + *contract.event("EV02").unwrap().first().unwrap(), + alloy_json_abi::Event { + name: "EV02".into(), + inputs: vec![eparam("uint ", false), eparam("bool b", false)], + anonymous: false, + } + ); + assert_eq!( + *contract.event("EV10").unwrap().first().unwrap(), + alloy_json_abi::Event { name: "EV10".into(), inputs: vec![], anonymous: true } + ); + assert_eq!( + *contract.event("EV11").unwrap().first().unwrap(), + alloy_json_abi::Event { + name: "EV11".into(), + inputs: vec![eparam("uint a", true)], + anonymous: true, + } + ); + assert_eq!( + *contract.event("EV12").unwrap().first().unwrap(), + alloy_json_abi::Event { + name: "EV12".into(), + inputs: vec![eparam("uint ", false), eparam("bool b", true)], + anonymous: true, + } + ); + assert_eq!( + contract.events, + abi_map! { + "EV00" => Contract::EV00::abi(), + "EV01" => Contract::EV01::abi(), + "EV02" => Contract::EV02::abi(), + "EV10" => Contract::EV10::abi(), + "EV11" => Contract::EV11::abi(), + "EV12" => Contract::EV12::abi(), + } + ); + + assert_eq!(contract.errors, Contract::abi::errors()); + assert_eq!( + *contract.error("ER0").unwrap().first().unwrap(), + Error { name: "ER0".into(), inputs: vec![] } + ); + assert_eq!( + *contract.error("ER1").unwrap().first().unwrap(), + Error { name: "ER1".into(), inputs: vec![param("uint a")] } + ); + assert_eq!( + *contract.error("ER2").unwrap().first().unwrap(), + Error { name: "ER2".into(), inputs: vec![param("uint "), param("bool b")] } + ); + assert_eq!( + contract.errors, + abi_map! { + "ER0" => Contract::ER0::abi(), + "ER1" => Contract::ER1::abi(), + "ER2" => Contract::ER2::abi(), + } + ); + + macro_rules! eq_modules { + ($($items:ident),* $(,)?) => {$( + assert_eq!(Contract::$items::abi(), not_contract::$items::abi()); + )*}; + } + eq_modules!( + EV00, EV01, EV02, EV10, EV11, EV12, ER0, ER1, ER2, F00Call, F01Call, F02Call, F10Call, + F11Call, F12Call, F20Call, F21Call, F22Call + ); +} + +#[test] +fn recursive() { + sol! { + #![sol(abi)] + + enum AccountAccessKind { + Call, + DelegateCall, + CallCode, + StaticCall, + Create, + SelfDestruct, + Resume, + } + + struct ChainInfo { + uint256 forkId; + uint256 chainId; + } + + struct AccountAccess { + ChainInfo chainInfo; + AccountAccessKind kind; + address account; + address accessor; + bool initialized; + uint256 oldBalance; + uint256 newBalance; + bytes deployedCode; + uint256 value; + bytes data; + bool reverted; + StorageAccess[] storageAccesses; + } + + struct StorageAccess { + address account; + bytes32 slot; + bool isWrite; + bytes32 previousValue; + bytes32 newValue; + bool reverted; + } + + function stopAndReturnStateDiff() external returns (AccountAccess[] memory accesses); + } + + let chain_info = Param { + ty: "tuple".into(), + name: "chainInfo".into(), + components: vec![ + param("uint256 "), // forkId + param("uint256 "), // chainId + ], + internal_type: None, + }; + let storage_accesses = Param { + ty: "tuple[]".into(), + name: "storageAccesses".into(), + components: vec![ + param("address "), // account + param("bytes32 "), // slot + param("bool "), // isWrite + param("bytes32 "), // previousValue + param("bytes32 "), // newValue + param("bool "), // reverted + ], + internal_type: None, + }; + assert_eq!( + stopAndReturnStateDiffCall::abi(), + Function { + name: "stopAndReturnStateDiff".into(), + inputs: vec![], + outputs: vec![Param { + ty: "tuple[]".into(), + name: "accesses".into(), + components: vec![ + chain_info, + param("uint8 kind"), // TODO: enum + param("address account"), + param("address accessor"), + param("bool initialized"), + param("uint256 oldBalance"), + param("uint256 newBalance"), + param("bytes deployedCode"), + param("uint256 value"), + param("bytes data"), + param("bool reverted"), + storage_accesses, + ], + internal_type: None, + }], + state_mutability: StateMutability::NonPayable, + } + ); +} + +sol! { + #![sol(abi)] + + contract Contract { + struct CustomStruct { + uint custom; + } + + struct CustomStruct2 { + CustomStruct cs; + bool cb; + } + + event EV00(); + event EV01(uint a); + event EV02(uint, bool b); + + event EV10() anonymous; + event EV11(uint indexed a) anonymous; + event EV12(uint, bool indexed b) anonymous; + + error ER0(); + error ER1(uint a); + error ER2(uint, bool b); + + constructor ctor(); + fallback(); + receive(); + + function F00(); + function F01(uint a) payable; + function F02(uint, bool b) view; + + function F10() pure; + function F11(uint a) returns (uint a); + function F12(uint, bool b) returns (uint, bool b); + + function F20(uint, uint[], uint[][1]); + function F21(CustomStruct, CustomStruct[], CustomStruct[][2]); + function F22(CustomStruct2, CustomStruct2[], CustomStruct2[][3]); + } +} + +mod not_contract { + use super::*; + + sol! { + #![sol(abi)] + + struct CustomStruct { + uint custom; + } + + struct CustomStruct2 { + CustomStruct cs; + bool cb; + } + + event EV00(); + event EV01(uint a); + event EV02(uint, bool b); + + event EV10() anonymous; + event EV11(uint indexed a) anonymous; + event EV12(uint, bool indexed b) anonymous; + + error ER0(); + error ER1(uint a); + error ER2(uint, bool b); + + function F00(); + function F01(uint a) payable; + function F02(uint, bool b) view; + + function F10() pure; + function F11(uint a) returns (uint a); + function F12(uint, bool b) returns (uint, bool b); + + function F20(uint, uint[], uint[][1]); + function F21(CustomStruct, CustomStruct[], CustomStruct[][2]); + function F22(CustomStruct2, CustomStruct2[], CustomStruct2[][3]); + } +} + +fn param(s: &str) -> Param { + let (ty, name) = s.split_once(' ').unwrap(); + Param { ty: ty.into(), name: name.into(), internal_type: None, components: vec![] } +} + +fn eparam(s: &str, indexed: bool) -> EventParam { + let (ty, name) = s.split_once(' ').unwrap(); + EventParam { + ty: ty.into(), + name: name.into(), + internal_type: None, + components: vec![], + indexed, + } +} diff --git a/crates/sol-types/tests/macros/sol/mod.rs b/crates/sol-types/tests/macros/sol/mod.rs index 46bdb3975..8d5526dca 100644 --- a/crates/sol-types/tests/macros/sol/mod.rs +++ b/crates/sol-types/tests/macros/sol/mod.rs @@ -3,6 +3,8 @@ use alloy_sol_types::{sol, SolCall, SolError, SolEvent, SolStruct, SolType}; use serde::Serialize; use serde_json::Value; +#[cfg(feature = "json")] +mod abi; #[cfg(feature = "json")] mod json; diff --git a/crates/sol-types/tests/ui/features/json/abi.rs b/crates/sol-types/tests/ui/features/json/abi.rs new file mode 100644 index 000000000..760bb719e --- /dev/null +++ b/crates/sol-types/tests/ui/features/json/abi.rs @@ -0,0 +1,49 @@ +use alloy_sol_types::sol; + +compile_error!("this is ok"); + +sol! { + #![sol(abi)] + + contract C { + event EV0(); + event EV1(uint a); + event EV2(uint, bool b); + + error ER0(); + error ER1(uint a); + error ER2(uint, bool b); + + function F00(); + function F01(uint a); + function F02(uint, bool b); + + function F11(uint a) returns (uint a); + function F12(uint, bool b) returns (uint, bool b); + } +} + +mod other { + use super::*; + + sol! { + #![sol(abi)] + + event EV0(); + event EV1(uint a); + event EV2(uint, bool b); + + error ER0(); + error ER1(uint a); + error ER2(uint, bool b); + + function F00(); + function F01(uint a); + function F02(uint, bool b); + + function F11(uint a) returns (uint a); + function F12(uint, bool b) returns (uint, bool b); + } +} + +fn main() {} diff --git a/crates/sol-types/tests/ui/features/json/abi.stderr b/crates/sol-types/tests/ui/features/json/abi.stderr new file mode 100644 index 000000000..6733d5900 --- /dev/null +++ b/crates/sol-types/tests/ui/features/json/abi.stderr @@ -0,0 +1,5 @@ +error: this is ok + --> tests/ui/features/json/abi.rs:3:1 + | +3 | compile_error!("this is ok"); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/crates/sol-types/tests/ui/features/json/abigen.stderr b/crates/sol-types/tests/ui/features/json/abigen.stderr index ce3d22651..8c69702c2 100644 --- a/crates/sol-types/tests/ui/features/json/abigen.stderr +++ b/crates/sol-types/tests/ui/features/json/abigen.stderr @@ -1,29 +1,29 @@ error: empty input is not allowed - --> tests/ui/json/abigen.rs:3:16 + --> tests/ui/features/json/abigen.rs:3:16 | 3 | sol!(EmptyStr, ""); | ^^ error: failed to canonicalize path: No such file or directory (os error 2) - --> tests/ui/json/abigen.rs:5:24 + --> tests/ui/features/json/abigen.rs:5:24 | 5 | sol!(PathDoesNotExist, "???"); | ^^^^^ error: failed to canonicalize path: No such file or directory (os error 2) - --> tests/ui/json/abigen.rs:6:6 + --> tests/ui/features/json/abigen.rs:6:6 | 6 | sol!("pragma solidity ^0.8.0"); | ^^^^^^^^^^^^^^^^^^^^^^^^ error: failed to canonicalize path: No such file or directory (os error 2) - --> tests/ui/json/abigen.rs:7:6 + --> tests/ui/features/json/abigen.rs:7:6 | 7 | sol!("pragma solidity ^0.8.0;"); | ^^^^^^^^^^^^^^^^^^^^^^^^^ -error: invalid JSON: missing field `abi` at line 1 column 2 - --> tests/ui/json/abigen.rs:9:22 +error: ABI not found in JSON + --> tests/ui/features/json/abigen.rs:9:6 | 9 | sol!(NoJsonFeature1, "{}"); - | ^^^^ + | ^^^^^^^^^^^^^^ diff --git a/crates/sol-types/tests/ui/features/not(json)/abi.rs b/crates/sol-types/tests/ui/features/not(json)/abi.rs new file mode 100644 index 000000000..a018f2b0d --- /dev/null +++ b/crates/sol-types/tests/ui/features/not(json)/abi.rs @@ -0,0 +1,49 @@ +use alloy_sol_types::sol; + +sol! { + #![sol(abi)] + + contract C { + event EV0(); + event EV1(uint a); + event EV2(uint, bool b); + + error ER0(); + error ER1(uint a); + error ER2(uint, bool b); + + function F00(); + function F01(uint a); + function F02(uint, bool b); + + function F10(); + function F11(uint a) returns (uint a); + function F12(uint, bool b) returns (uint, bool b); + } +} + +mod other { + use super::*; + + sol! { + #![sol(abi)] + + event EV0(); + event EV1(uint a); + event EV2(uint, bool b); + + error ER0(); + error ER1(uint a); + error ER2(uint, bool b); + + function F00(); + function F01(uint a); + function F02(uint, bool b); + + function F10(); + function F11(uint a) returns (uint a); + function F12(uint, bool b) returns (uint, bool b); + } +} + +fn main() {} diff --git a/crates/sol-types/tests/ui/features/not(json)/abi.stderr b/crates/sol-types/tests/ui/features/not(json)/abi.stderr new file mode 100644 index 000000000..70c3a1e20 --- /dev/null +++ b/crates/sol-types/tests/ui/features/not(json)/abi.stderr @@ -0,0 +1,13 @@ +error: the `#[sol(dyn_abi)]` attribute requires the `"json"` feature + --> tests/ui/features/not(json)/abi.rs:3:1 + | +3 | / sol! { +4 | | #![sol(abi)] +5 | | +6 | | contract C { +... | +22 | | } +23 | | } + | |_^ + | + = note: this error originates in the macro `sol` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/sol-types/tests/ui/features/not(json)/abigen.stderr b/crates/sol-types/tests/ui/features/not(json)/abigen.stderr index 2318456a9..0cd8289f1 100644 --- a/crates/sol-types/tests/ui/features/not(json)/abigen.stderr +++ b/crates/sol-types/tests/ui/features/not(json)/abigen.stderr @@ -1,41 +1,41 @@ error: empty input is not allowed - --> tests/ui/not(json)/abigen.rs:3:16 + --> tests/ui/features/not(json)/abigen.rs:3:16 | 3 | sol!(EmptyStr, ""); | ^^ error: failed to canonicalize path: No such file or directory (os error 2) - --> tests/ui/not(json)/abigen.rs:5:24 + --> tests/ui/features/not(json)/abigen.rs:5:24 | 5 | sol!(PathDoesNotExist, "???"); | ^^^^^ error: failed to canonicalize path: No such file or directory (os error 2) - --> tests/ui/not(json)/abigen.rs:6:6 + --> tests/ui/features/not(json)/abigen.rs:6:6 | 6 | sol!("pragma solidity ^0.8.0"); | ^^^^^^^^^^^^^^^^^^^^^^^^ error: failed to canonicalize path: No such file or directory (os error 2) - --> tests/ui/not(json)/abigen.rs:7:6 + --> tests/ui/features/not(json)/abigen.rs:7:6 | 7 | sol!("pragma solidity ^0.8.0;"); | ^^^^^^^^^^^^^^^^^^^^^^^^^ -error: JSON support must be enabled with the `json` feature - --> tests/ui/not(json)/abigen.rs:9:22 +error: JSON support must be enabled with the "json" feature + --> tests/ui/features/not(json)/abigen.rs:9:22 | 9 | sol!(NoJsonFeature1, "{}"); | ^^^^ -error: JSON support must be enabled with the `json` feature - --> tests/ui/not(json)/abigen.rs:10:22 +error: JSON support must be enabled with the "json" feature + --> tests/ui/features/not(json)/abigen.rs:10:22 | 10 | sol!(NoJsonFeature2, "{ \"abi\": [] }"); | ^^^^^^^^^^^^^^^^^ -error: JSON support must be enabled with the `json` feature - --> tests/ui/not(json)/abigen.rs:11:22 +error: JSON support must be enabled with the "json" feature + --> tests/ui/features/not(json)/abigen.rs:11:22 | 11 | sol!(NoJsonFeature3, "[]"); | ^^^^ diff --git a/crates/syn-solidity/README.md b/crates/syn-solidity/README.md index 37a70acfb..79b838662 100644 --- a/crates/syn-solidity/README.md +++ b/crates/syn-solidity/README.md @@ -93,7 +93,7 @@ let Some(Item::Function(function)) = body.first() else { }; assert_eq!(function.attrs.len(), 1); // doc comment assert_eq!(function.name.as_ref().unwrap(), "helloWorld"); -assert!(function.arguments.is_empty()); // () +assert!(function.parameters.is_empty()); // () assert_eq!(function.attributes.len(), 2); // external pure assert!(function.returns.is_some()); diff --git a/crates/syn-solidity/src/item/function.rs b/crates/syn-solidity/src/item/function.rs index a4d0eccab..f8a8bed0b 100644 --- a/crates/syn-solidity/src/item/function.rs +++ b/crates/syn-solidity/src/item/function.rs @@ -29,7 +29,7 @@ pub struct ItemFunction { /// Parens are optional for modifiers: /// pub paren_token: Option, - pub arguments: ParameterList, + pub parameters: ParameterList, /// The Solidity attributes of the function. pub attributes: FunctionAttributes, /// The optional return types of the function. @@ -44,7 +44,7 @@ impl fmt::Display for ItemFunction { f.write_str(" ")?; name.fmt(f)?; } - write!(f, "({})", self.arguments)?; + write!(f, "({})", self.parameters)?; if !self.attributes.is_empty() { write!(f, " {}", self.attributes)?; @@ -67,7 +67,7 @@ impl fmt::Debug for ItemFunction { .field("attrs", &self.attrs) .field("kind", &self.kind) .field("name", &self.name) - .field("arguments", &self.arguments) + .field("arguments", &self.parameters) .field("attributes", &self.attributes) .field("returns", &self.returns) .field("body", &self.body) @@ -81,7 +81,7 @@ impl Parse for ItemFunction { let kind: FunctionKind = input.parse()?; let name = input.call(SolIdent::parse_opt)?; - let (paren_token, arguments) = if kind.is_modifier() && !input.peek(Paren) { + let (paren_token, parameters) = if kind.is_modifier() && !input.peek(Paren) { (None, ParameterList::new()) } else { let content; @@ -92,7 +92,7 @@ impl Parse for ItemFunction { let returns = input.call(Returns::parse_opt)?; let body = input.parse()?; - Ok(Self { attrs, kind, name, paren_token, arguments, attributes, returns, body }) + Ok(Self { attrs, kind, name, paren_token, parameters, attributes, returns, body }) } } @@ -122,7 +122,7 @@ impl ItemFunction { kind, name, paren_token: Some(Paren(span)), - arguments: Parameters::new(), + parameters: Parameters::new(), attributes: FunctionAttributes::new(), returns: None, body: FunctionBody::Empty(Token![;](span)), @@ -161,14 +161,14 @@ impl ItemFunction { // mapping(k => v) -> arguments += k, ty = v Type::Mapping(map) => { let key = VariableDeclaration::new_with(*map.key, None, map.key_name); - function.arguments.push(key); + function.parameters.push(key); return_name = map.value_name; ty = *map.value; } // inner[] -> arguments += uint256, ty = inner Type::Array(array) => { let uint256 = Type::Uint(span, NonZeroU16::new(256)); - function.arguments.push(VariableDeclaration::new(uint256)); + function.parameters.push(VariableDeclaration::new(uint256)); ty = *array.ty; } _ => break, @@ -221,7 +221,7 @@ impl ItemFunction { /// Returns the function's arguments tuple type. pub fn call_type(&self) -> Type { - Type::Tuple(self.arguments.types().cloned().collect()) + Type::Tuple(self.parameters.types().cloned().collect()) } /// Returns the function's return tuple type. diff --git a/crates/syn-solidity/src/macros.rs b/crates/syn-solidity/src/macros.rs index a651cc868..09a2eb6cb 100644 --- a/crates/syn-solidity/src/macros.rs +++ b/crates/syn-solidity/src/macros.rs @@ -386,7 +386,7 @@ macro_rules! make_visitor { if let Some(name) = & $($mut)? function.name { v.visit_ident(name); } - v.visit_parameter_list(& $($mut)? function.arguments); + v.visit_parameter_list(& $($mut)? function.parameters); if let Some(returns) = & $($mut)? function.returns { v.visit_parameter_list(& $($mut)? returns.returns); } @@ -476,8 +476,7 @@ macro_rules! kw_enum { $(#[$attr])* #[derive(Clone, Copy)] $vis enum $name {$( - #[doc = concat!("`", stringify!($kw), "`")] - /// + #[doc = concat!("`", stringify!($kw), "`\n\n")] $(#[$variant_attr])* $variant($crate::kw::$kw), )+} @@ -615,8 +614,7 @@ macro_rules! op_enum { $(#[$attr])* #[derive(Clone, Copy)] $vis enum $name {$( - #[doc = concat!("`", $(stringify!($op),)+ "`")] - /// + #[doc = concat!("`", $(stringify!($op),)+ "`\n\n")] $(#[$variant_attr])* $variant($(::syn::Token![$op]),+), )+} diff --git a/crates/syn-solidity/src/type/mod.rs b/crates/syn-solidity/src/type/mod.rs index 68adcc415..9049b1912 100644 --- a/crates/syn-solidity/src/type/mod.rs +++ b/crates/syn-solidity/src/type/mod.rs @@ -2,6 +2,7 @@ use crate::{kw, sol_path, SolPath, Spanned}; use proc_macro2::Span; use std::{ fmt, + fmt::Write, hash::{Hash, Hasher}, num::{IntErrorKind, NonZeroU16}, }; @@ -374,6 +375,40 @@ impl Type { } } + /// Returns the inner type. + pub fn peel_arrays(&self) -> &Self { + let mut this = self; + while let Self::Array(array) = this { + this = &array.ty; + } + this + } + + /// Returns the Solidity ABI name for this type. This is `tuple` for custom types, otherwise the + /// same as [`Display`](fmt::Display). + pub fn abi_name(&self) -> String { + let mut s = String::new(); + self.abi_name_raw(&mut s); + s + } + + /// Returns the Solidity ABI name for this type. This is `tuple` for custom types, otherwise the + /// same as [`Display`](fmt::Display). + pub fn abi_name_raw(&self, s: &mut String) { + match self { + Self::Custom(_) => s.push_str("tuple"), + Self::Array(array) => { + array.ty.abi_name_raw(s); + if let Some(size) = array.size() { + write!(s, "[{size}]").unwrap(); + } else { + s.push_str("[]"); + } + } + _ => write!(s, "{self}").unwrap(), + } + } + /// Traverses this type while calling `f`. #[cfg(feature = "visit")] pub fn visit(&self, f: impl FnMut(&Self)) { @@ -384,6 +419,12 @@ impl Type { (self.0)(ty); crate::visit::visit_type(self, ty); } + // Reduce generated code size by explicitly implementing these methods as noops. + fn visit_block(&mut self, _block: &crate::Block) {} + fn visit_expr(&mut self, _expr: &crate::Expr) {} + fn visit_stmt(&mut self, _stmt: &crate::Stmt) {} + fn visit_file(&mut self, _file: &crate::File) {} + fn visit_item(&mut self, _item: &crate::Item) {} } VisitType(f).visit_type(self); } @@ -398,6 +439,12 @@ impl Type { (self.0)(ty); crate::visit_mut::visit_type(self, ty); } + // Reduce generated code size by explicitly implementing these methods as noops. + fn visit_block(&mut self, _block: &mut crate::Block) {} + fn visit_expr(&mut self, _expr: &mut crate::Expr) {} + fn visit_stmt(&mut self, _stmt: &mut crate::Stmt) {} + fn visit_file(&mut self, _file: &mut crate::File) {} + fn visit_item(&mut self, _item: &mut crate::Item) {} } VisitTypeMut(f).visit_type(self); }