diff --git a/crates/json-abi/src/to_sol.rs b/crates/json-abi/src/to_sol.rs index 2e159a1d0..01013b0ba 100644 --- a/crates/json-abi/src/to_sol.rs +++ b/crates/json-abi/src/to_sol.rs @@ -21,6 +21,7 @@ pub struct ToSolConfig { enums_as_udvt: bool, for_sol_macro: bool, one_contract: bool, + standalone_globals: bool, } impl Default for ToSolConfig { @@ -39,6 +40,7 @@ impl ToSolConfig { enums_as_udvt: true, for_sol_macro: false, one_contract: false, + standalone_globals: false, } } @@ -73,6 +75,13 @@ impl ToSolConfig { self.one_contract = yes; self } + + /// Sets whether globals should be emitted at the root of the output. + /// Default: `false`. + pub const fn standalone_globals(mut self, yes: bool) -> Self { + self.standalone_globals = yes; + self + } } pub(crate) trait ToSol { @@ -92,6 +101,9 @@ pub(crate) struct SolPrinter<'a> { /// Configuration. config: ToSolConfig, + + /// Current indentation level. + indent_level: usize, } impl Deref for SolPrinter<'_> { @@ -112,7 +124,7 @@ impl DerefMut for SolPrinter<'_> { impl<'a> SolPrinter<'a> { pub(crate) fn new(s: &'a mut String, name: &'a str, config: ToSolConfig) -> Self { - Self { s, name, print_param_location: false, config } + Self { s, name, print_param_location: false, config, indent_level: 0 } } pub(crate) fn print(&mut self, abi: &'a JsonAbi) { @@ -120,7 +132,9 @@ impl<'a> SolPrinter<'a> { } fn indent(&mut self) { - self.push_str(" "); + for _ in 0..self.indent_level { + self.push_str(" "); + } } /// Normalizes `s` as a Rust identifier and pushes it to the buffer. @@ -163,7 +177,8 @@ impl JsonAbi { }; } - let mut its = InternalTypes::new(out.name, out.config.enums_as_udvt); + let mut its = + InternalTypes::new(out.name, out.config.enums_as_udvt, out.config.standalone_globals); its.visit_abi(self); let one_contract = out.config.one_contract; @@ -177,11 +192,13 @@ impl JsonAbi { out.push_str(name); out.push_str(" {\n"); let prev = core::mem::replace(&mut out.name, name); + out.indent_level += 1; for it in its { out.indent(); it.to_sol(out); out.push('\n'); } + out.indent_level -= 1; out.name = prev; out.push_str("}\n\n"); } @@ -195,6 +212,7 @@ impl JsonAbi { out.push('{'); out.push('\n'); + out.indent_level += 1; if one_contract { for (name, its) in &its.other { if its.is_empty() { @@ -220,6 +238,14 @@ impl JsonAbi { out.pop(); // trailing newline out.push('}'); + out.indent_level -= 1; + + if !its.globals.is_empty() { + out.push('\n'); + fmt!(its.globals); + out.pop(); // trailing newline + out.pop(); // trailing newline + } } } @@ -228,13 +254,22 @@ struct InternalTypes<'a> { name: &'a str, this_its: BTreeSet>, other: BTreeMap<&'a String, BTreeSet>>, + globals: BTreeSet>, enums_as_udvt: bool, + standalone_globals: bool, } impl<'a> InternalTypes<'a> { #[allow(clippy::missing_const_for_fn)] - fn new(name: &'a str, enums_as_udvt: bool) -> Self { - Self { name, this_its: BTreeSet::new(), other: BTreeMap::new(), enums_as_udvt } + fn new(name: &'a str, enums_as_udvt: bool, standalone_globals: bool) -> Self { + Self { + name, + this_its: BTreeSet::new(), + other: BTreeMap::new(), + globals: BTreeSet::new(), + enums_as_udvt, + standalone_globals, + } } fn visit_abi(&mut self, abi: &'a JsonAbi) { @@ -312,6 +347,8 @@ impl<'a> InternalTypes<'a> { } else { self.other.entry(contract).or_default().insert(it); } + } else if self.standalone_globals { + self.globals.insert(it); } else { self.this_its.insert(it); } @@ -388,12 +425,13 @@ impl ToSol for It<'_> { out.push_str("struct "); out.push_ident(self.name); out.push_str(" {\n"); + out.indent_level += 1; for component in components { - out.indent(); out.indent(); component.to_sol(out); out.push_str(";\n"); } + out.indent_level -= 1; out.indent(); out.push('}'); } diff --git a/crates/json-abi/tests/abi/ContractUsingGlobals.json b/crates/json-abi/tests/abi/ContractUsingGlobals.json new file mode 100644 index 000000000..bfa337389 --- /dev/null +++ b/crates/json-abi/tests/abi/ContractUsingGlobals.json @@ -0,0 +1,88 @@ +[ + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "enum GlobalEnum", + "name": "enum_", + "type": "uint8" + } + ], + "internalType": "struct GlobalStruct", + "name": "payload", + "type": "tuple" + } + ], + "name": "GlobalError", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "enum GlobalEnum", + "name": "enum_", + "type": "uint8" + } + ], + "indexed": false, + "internalType": "struct GlobalStruct", + "name": "payload", + "type": "tuple" + }, + { + "indexed": false, + "internalType": "GlobalUDT", + "name": "amount", + "type": "uint256" + } + ], + "name": "GlobalEvent", + "type": "event" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "enum GlobalEnum", + "name": "kind", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "count", + "type": "uint256" + } + ], + "internalType": "struct Interface.InterfaceStruct", + "name": "data", + "type": "tuple" + } + ], + "name": "emitGlobalEvent", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "triggerError", + "outputs": [], + "stateMutability": "pure", + "type": "function" + } +] \ No newline at end of file diff --git a/crates/json-abi/tests/abi/ContractUsingGlobals.sol b/crates/json-abi/tests/abi/ContractUsingGlobals.sol new file mode 100644 index 000000000..21c8f97c5 --- /dev/null +++ b/crates/json-abi/tests/abi/ContractUsingGlobals.sol @@ -0,0 +1,22 @@ +library Interface { + struct InterfaceStruct { + GlobalEnum kind; + uint256 count; + } +} + +interface ContractUsingGlobals { + type GlobalEnum is uint8; + type GlobalUDT is uint256; + struct GlobalStruct { + uint256 value; + GlobalEnum enum_; + } + + error GlobalError(GlobalStruct payload); + + event GlobalEvent(GlobalStruct payload, GlobalUDT amount); + + function emitGlobalEvent(Interface.InterfaceStruct memory data) external; + function triggerError() external pure; +} \ No newline at end of file diff --git a/crates/json-abi/tests/abi/Handler.json b/crates/json-abi/tests/abi/Handler.json new file mode 100644 index 000000000..1fc54bdab --- /dev/null +++ b/crates/json-abi/tests/abi/Handler.json @@ -0,0 +1,29 @@ +[ + { + "type": "function", + "name": "handle", + "inputs": [ + { + "name": "foobar", + "type": "tuple", + "internalType": "struct IHandler.FooBar", + "components": [ + { + "name": "foo", + "type": "tuple", + "internalType": "struct Foo", + "components": [ + { + "name": "newNumber", + "type": "uint256", + "internalType": "uint256" + } + ] + } + ] + } + ], + "outputs": [], + "stateMutability": "nonpayable" + } +] \ No newline at end of file diff --git a/crates/json-abi/tests/abi/Handler.sol b/crates/json-abi/tests/abi/Handler.sol new file mode 100644 index 000000000..9296b3de9 --- /dev/null +++ b/crates/json-abi/tests/abi/Handler.sol @@ -0,0 +1,13 @@ +library IHandler { + struct FooBar { + Foo foo; + } +} + +interface Handler { + struct Foo { + uint256 newNumber; + } + + function handle(IHandler.FooBar memory foobar) external; +} \ No newline at end of file diff --git a/crates/sol-macro-input/src/attr.rs b/crates/sol-macro-input/src/attr.rs index da592c0fd..a4ff1cd6f 100644 --- a/crates/sol-macro-input/src/attr.rs +++ b/crates/sol-macro-input/src/attr.rs @@ -114,6 +114,10 @@ pub struct SolAttrs { /// Ignore unlinked bytecode /// `#[sol(ignore_unlinked)]` pub ignore_unlinked: Option, + + /// Emit globals at the root when generating from JSON ABI. + /// `#[sol(standalone_globals)]` + pub standalone_globals: Option, } impl SolAttrs { @@ -206,6 +210,7 @@ impl SolAttrs { type_check => lit()?, ignore_unlinked => bool()?, + standalone_globals => bool()?, }; Ok(()) })?; @@ -242,6 +247,7 @@ impl SolAttrs { merge_opt(&mut a.deployed_bytecode, &b.deployed_bytecode); merge_opt(&mut a.type_check, &b.type_check); merge_opt(&mut a.ignore_unlinked, &b.ignore_unlinked); + merge_opt(&mut a.standalone_globals, &b.standalone_globals); } } @@ -466,6 +472,10 @@ mod tests { #[sol(rpc = true)] => Ok(sol_attrs! { rpc: true }), #[sol(rpc = false)] => Ok(sol_attrs! { rpc: false }), + #[sol(standalone_globals)] => Ok(sol_attrs! { standalone_globals: true }), + #[sol(standalone_globals = true)] => Ok(sol_attrs! { standalone_globals: true }), + #[sol(standalone_globals = false)] => Ok(sol_attrs! { standalone_globals: false }), + #[sol(alloy_sol_types)] => Err("expected `=`"), #[sol(alloy_sol_types = alloy_core::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(alloy_core::sol_types) }), #[sol(alloy_sol_types = ::alloy_core::sol_types)] => Ok(sol_attrs! { alloy_sol_types: parse_quote!(::alloy_core::sol_types) }), diff --git a/crates/sol-macro-input/src/json.rs b/crates/sol-macro-input/src/json.rs index 13906b0ac..c90ccb930 100644 --- a/crates/sol-macro-input/src/json.rs +++ b/crates/sol-macro-input/src/json.rs @@ -1,4 +1,4 @@ -use crate::{SolInput, SolInputKind}; +use crate::{SolAttrs, SolInput, SolInputKind}; use alloy_json_abi::{ContractObject, JsonAbi, ToSolConfig}; use proc_macro2::{Ident, TokenStream, TokenTree}; use quote::quote; @@ -17,8 +17,22 @@ impl SolInput { }; let mut abi = abi.ok_or_else(|| syn::Error::new(name.span(), "ABI not found in JSON"))?; - let sol = abi_to_sol(&name, &mut abi); - let mut all_tokens = tokens_for_sol(&name, &sol)?.into_iter(); + let (sol_attrs, _) = SolAttrs::parse(&attrs)?; + let standalone_globals = sol_attrs.standalone_globals.unwrap_or(false); + let config = ToSolConfig::new() + .print_constructors(true) + .for_sol_macro(true) + .standalone_globals(standalone_globals); + let sol = abi_to_sol(&name, &mut abi, config); + let all_tokens = tokens_for_sol(&name, &sol)?; + let mut ast: ast::File = syn::parse2(all_tokens).map_err(|e| { + let msg = format!( + "failed to parse ABI-generated tokens into a Solidity AST for `{name}`: {e}.\n\ + This is a bug. We would appreciate a bug report: \ + https://github.com/alloy-rs/core/issues/new/choose" + ); + syn::Error::new(name.span(), msg) + })?; let (inner_attrs, attrs) = attrs .into_iter() @@ -26,34 +40,6 @@ impl SolInput { let (derives, sol_derives) = extract_derive_attrs(&attrs); - let mut library_tokens_iter = all_tokens - .by_ref() - .take_while(|tt| !matches!(tt, TokenTree::Ident(id) if id == "interface")) - .skip_while(|tt| matches!(tt, TokenTree::Ident(id) if id == "library")) - .peekable(); - - let library_tokens = library_tokens_iter.by_ref(); - - let mut libraries = Vec::new(); - - while library_tokens.peek().is_some() { - let sol_library_tokens: TokenStream = std::iter::once(TokenTree::Ident(id("library"))) - .chain( - library_tokens - .take_while(|tt| !matches!(tt, TokenTree::Ident(id) if id == "library")), - ) - .collect(); - - let tokens = quote! { - #(#derives)* - #(#sol_derives)* - #sol_library_tokens - }; - - libraries.push(tokens); - } - let sol_interface_tokens: TokenStream = - std::iter::once(TokenTree::Ident(id("interface"))).chain(all_tokens).collect(); let bytecode = bytecode.map(|bytes| { let s = bytes.to_string(); quote!(bytecode = #s,) @@ -63,49 +49,38 @@ impl SolInput { quote!(deployed_bytecode = #s) }); - let attrs_iter = attrs.iter(); - let doc_str = format!( - "\n\n\ -Generated by the following Solidity interface... -```solidity -{sol} -``` - -...which was generated by the following JSON ABI: -```json -{json_s} -```", - json_s = serde_json::to_string_pretty(&abi).unwrap() - ); - let tokens = quote! { - #(#inner_attrs)* - #(#libraries)* - - #(#attrs_iter)* - #[doc = #doc_str] - #[sol(#bytecode #deployed_bytecode)] - #sol_interface_tokens + let ctx = ApplyAttrsCtx { + derives: &derives, + sol_derives: &sol_derives, + interface_attrs: &attrs, + bytecode: bytecode.as_ref(), + deployed_bytecode: deployed_bytecode.as_ref(), + sol: &sol, + abi: &abi, }; - - let ast: ast::File = syn::parse2(tokens).map_err(|e| { - let msg = format!( - "failed to parse ABI-generated tokens into a Solidity AST for `{name}`: {e}.\n\ - This is a bug. We would appreciate a bug report: \ - https://github.com/alloy-rs/core/issues/new/choose" - ); - syn::Error::new(name.span(), msg) - })?; + apply_attrs_to_items(&mut ast.items, &ctx); + ast.attrs.extend(inner_attrs); let kind = SolInputKind::Sol(ast); Ok(SolInput { attrs, path, kind }) } } +/// Shared context for applying user attributes to ABI-derived items. +struct ApplyAttrsCtx<'a> { + derives: &'a [&'a syn::Attribute], + sol_derives: &'a [&'a syn::Attribute], + interface_attrs: &'a [syn::Attribute], + bytecode: Option<&'a TokenStream>, + deployed_bytecode: Option<&'a TokenStream>, + sol: &'a str, + abi: &'a JsonAbi, +} + // doesn't parse Json -fn abi_to_sol(name: &Ident, abi: &mut JsonAbi) -> String { +fn abi_to_sol(name: &Ident, abi: &mut JsonAbi, config: ToSolConfig) -> String { abi.dedup(); - let config = ToSolConfig::new().print_constructors(true).for_sol_macro(true); abi.to_sol(&name.to_string(), Some(config)) } @@ -153,11 +128,75 @@ fn extract_derive_attrs(attrs: &[syn::Attribute]) -> (Vec<&syn::Attribute>, Vec< }) } -#[inline] -#[track_caller] -fn id(s: impl AsRef) -> Ident { - // Ident::new panics on Rust keywords and `r#` prefixes - syn::parse_str(s.as_ref()).unwrap() +/// Applies derive/`sol` attributes to ABI-derived items. +/// +/// - Non-interface contracts, structs, enums, and UDVTs get the user-specified derive and `sol` +/// attributes cloned onto them. +/// - The single interface gets the outer attributes, a generated doc (including the original +/// Solidity/JSON ABI), and the `#[sol(bytecode = ..., deployed_bytecode = ...)]` attribute. +fn apply_attrs_to_items(items: &mut [ast::Item], ctx: &ApplyAttrsCtx<'_>) { + for item in items { + match item { + ast::Item::Contract(contract) if contract.kind.is_interface() => { + apply_interface_attrs(contract, ctx); + } + ast::Item::Contract(contract) => { + extend_attrs(&mut contract.attrs, ctx.derives, ctx.sol_derives); + } + ast::Item::Struct(strukt) => { + extend_attrs(&mut strukt.attrs, ctx.derives, ctx.sol_derives); + } + ast::Item::Udt(udt) => { + extend_attrs(&mut udt.attrs, ctx.derives, ctx.sol_derives); + } + // Globals from `to_sol` are only structs, UDVTs; enums are flattened to `uint8`, + // while errors/functions/events,etc are emitted in the interface. + _ => debug_assert!(false, "unexpected global item type"), + } + } +} + +/// Merge user outer attrs with generated docs/metadata for the sole interface. +fn apply_interface_attrs(contract: &mut ast::ItemContract, ctx: &ApplyAttrsCtx<'_>) { + let bytecode = ctx.bytecode; + let deployed_bytecode = ctx.deployed_bytecode; + let doc_str = format!( + "\n\n\ +Generated by the following Solidity interface...\ +```solidity\ +{sol}\ +```\ +\n\ +...which was generated by the following JSON ABI:\ +```json\ +{json_s}\ +```", + sol = ctx.sol, + json_s = serde_json::to_string_pretty(ctx.abi).unwrap(), + ); + let doc_attr: syn::Attribute = syn::parse_quote!(#[doc = #doc_str]); + let sol_attr: syn::Attribute = syn::parse_quote!(#[sol(#bytecode #deployed_bytecode)]); + + let mut merged = ctx.interface_attrs.to_vec(); + merged.push(doc_attr); + merged.push(sol_attr); + contract.attrs = merged; +} + +/// Clone user-specified `derive`/`sol(...)` attributes onto the given item. +/// Used for globals (structs/UDVTs) and non-interface contracts emitted by `to_sol`. +fn extend_attrs( + attrs: &mut Vec, + derives: &[&syn::Attribute], + sol_derives: &[&syn::Attribute], +) { + attrs.reserve(derives.len() + sol_derives.len()); + for attr in derives { + attrs.push((*attr).clone()); + } + for attr in sol_derives { + attrs.push((*attr).clone()); + } } #[cfg(test)] @@ -165,6 +204,11 @@ mod tests { use super::*; use std::path::{Path, PathBuf}; + fn id(s: impl AsRef) -> Ident { + // Ident::new panics on Rust keywords and `r#` prefixes + syn::parse_str(s.as_ref()).unwrap() + } + #[test] #[cfg_attr(miri, ignore = "no fs")] fn abi() { @@ -189,7 +233,8 @@ mod tests { let name = Path::new(path).file_stem().unwrap().to_str().unwrap(); let name_id = id(name); - let sol = abi_to_sol(&name_id, &mut abi); + let config = ToSolConfig::new().print_constructors(true).for_sol_macro(true); + let sol = abi_to_sol(&name_id, &mut abi, config); let tokens = match tokens_for_sol(&name_id, &sol) { Ok(tokens) => tokens, Err(e) => { diff --git a/crates/sol-macro/src/lib.rs b/crates/sol-macro/src/lib.rs index 42fbece7c..d1b1044e0 100644 --- a/crates/sol-macro/src/lib.rs +++ b/crates/sol-macro/src/lib.rs @@ -158,6 +158,8 @@ use syn::parse_macro_input; /// - `type_check = ` (UDVT only): specifies a function to be used to check an User /// Defined Type. /// - `ignore_unlinked [ = ]`: ignores unlinked bytecode in contract artifacts. +/// - `standalone_globals [ = ]` (JSON ABI only): emits global structs/UDVTs at the +/// root of the generated Solidity instead of nesting them under the interface/contract. /// /// ### Structs and enums /// diff --git a/crates/sol-types/tests/macros/sol/json.rs b/crates/sol-types/tests/macros/sol/json.rs index 9ca56b08d..8739eaabc 100644 --- a/crates/sol-types/tests/macros/sol/json.rs +++ b/crates/sol-types/tests/macros/sol/json.rs @@ -283,3 +283,65 @@ fn ignore_unlinked_bytecode_attr() { let _ = AnotherUnlinked::addCall { a: U256::ZERO, b: U256::ZERO }; } + +// Ensure globals stay in the outer namespace and interface/contract items stay nested. +#[test] +fn contract_with_globals() { + mod container { + use super::*; + sol!( + #[derive(Debug, PartialEq, Eq)] + #[sol(standalone_globals)] + ContractUsingGlobals, + "../json-abi/tests/abi/ContractUsingGlobals.json" + ); + } + + // Globals should be available at the module scope, not under ContractWithGlobals. + let _udt: container::GlobalUDT = container::GlobalUDT::from(U256::from(123u64)); + let enm: container::GlobalEnum = container::GlobalEnum::from(1u8); + let gs = container::GlobalStruct { value: U256::from(1), enum_: enm.clone().into() }; + + // GlobalEnum is flattened to a UDVT over uint8 + let raw = enm.clone().into_underlying(); + assert_eq!(raw, 1u8); + + // Interface-scoped struct should live under the interface namespace. + let _iface_struct = + container::Interface::InterfaceStruct { kind: enm.into(), count: U256::from(2u64) }; + + // Events/errors belong to the contract (interface) namespace. + let _event_sig = ::SIGNATURE; + let _error_sig = ::SIGNATURE; + + // Basic equality and Debug derive on globals. + assert_eq!(gs, gs); + let _ = format!("{gs:?}"); +} + +#[test] +fn handler() { + mod container { + use super::*; + sol!( + #[sol(standalone_globals)] + Handler, + "../json-abi/tests/abi/Handler.json" + ); + } + use container::*; + + // Foo is a global struct, available at module scope. + let foo = Foo { newNumber: U256::from(1u64) }; + + // FooBar lives under the IHandler library namespace. + let foobar = IHandler::FooBar { foo }; + + // Calling the interface should accept the namespaced FooBar. + let call = Handler::handleCall { foobar }; + // Encode to ensure the generated types/namespaces are valid (should not panic). + let _encoded = call.abi_encode(); + + // Check the function selector/signature. + assert_eq!(Handler::handleCall::SIGNATURE, "handle(((uint256)))"); +}