diff --git a/crates/sol-macro-expander/src/expand/to_abi.rs b/crates/sol-macro-expander/src/expand/to_abi.rs index d0bf0a100..549df5659 100644 --- a/crates/sol-macro-expander/src/expand/to_abi.rs +++ b/crates/sol-macro-expander/src/expand/to_abi.rs @@ -1,7 +1,8 @@ use super::ExpCtxt; use crate::verbatim::Verbatim; use alloy_json_abi::{ - Constructor, Error, Event, EventParam, Fallback, Function, Param, Receive, StateMutability, + Constructor, Error, Event, EventParam, Fallback, Function, InternalType, Param, Receive, + StateMutability, }; use ast::{ItemError, ItemEvent, ItemFunction}; use proc_macro2::TokenStream; @@ -105,38 +106,101 @@ fn ty_to_param(name: Option, ty: &ast::Type, cx: &ExpCtxt<'_>) -> Param ty_name = format!("tuple{suffix}"); } - let mut component_names = vec![]; - let resolved = match ty.peel_arrays() { + // For struct types, get the original fields to preserve type information (like UDVTs) + let original_fields = 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(); + Some(s.fields.clone()) + } else { + None } - cx.custom_type(name) } + _ => None, + }; + + let resolved = match ty.peel_arrays() { + ast::Type::Custom(name) => cx.custom_type(name), ty => ty, }; - let components = if let ast::Type::Tuple(tuple) = resolved { - tuple - .types + let components = if let Some(fields) = original_fields { + // Use original struct fields to preserve UDVT and other custom type names + fields .iter() - .enumerate() - .map(|(i, ty)| ty_to_param(component_names.get(i).cloned(), ty, cx)) + .map(|field| ty_to_param(field.name.as_ref().map(|n| n.as_string()), &field.ty, cx)) .collect() + } else if let ast::Type::Tuple(tuple) = resolved { + // For non-struct tuples, use the resolved types + tuple.types.iter().map(|ty| ty_to_param(None, ty, cx)).collect() } else { vec![] }; - // TODO: internal_type - let internal_type = None; + let internal_type = ty_to_internal_type(ty, cx); Param { ty: ty_name, name: name.unwrap_or_default(), internal_type, components } } +/// Generates the internal type for a given Solidity type. +/// This represents the source-level type as it appears in the Solidity code. +fn ty_to_internal_type(ty: &ast::Type, cx: &ExpCtxt<'_>) -> Option { + // Collect array suffixes + let mut array_suffix = String::new(); + rec_ty_abi_string_suffix(cx, ty, &mut array_suffix); + + // Peel arrays to get the base type + let base_ty = ty.peel_arrays(); + + match base_ty { + ast::Type::Address(_, Some(_)) => { + // Address payable + Some(InternalType::AddressPayable(format!("address payable{array_suffix}"))) + } + ast::Type::Custom(path) => { + // Determine the contract qualifier. + let contract = if path.len() == 2 { + // Explicit namespace: MyContract.MyStruct + Some(path.first().as_string()) + } else if path.len() == 1 { + // Single component: check if we're in a contract namespace + // and if the item is defined in that namespace + cx.current_namespace.as_ref().map(|ns| ns.as_string()) + } else { + None + }; + + // Get the type name (last component of the path) + let type_name = path.last().as_string(); + + // Look up what kind of item this is + match cx.try_item(path) { + Some(ast::Item::Struct(_)) => Some(InternalType::Struct { + contract, + ty: format!("{type_name}{array_suffix}"), + }), + Some(ast::Item::Enum(_)) => { + Some(InternalType::Enum { contract, ty: format!("{type_name}{array_suffix}") }) + } + Some(ast::Item::Contract(_)) => { + Some(InternalType::Contract(format!("{type_name}{array_suffix}"))) + } + Some(ast::Item::Udt(_)) => { + Some(InternalType::Other { contract, ty: format!("{type_name}{array_suffix}") }) + } + _ => { + // Fallback for unresolved custom types + Some(InternalType::Other { contract, ty: format!("{type_name}{array_suffix}") }) + } + } + } + _ => { + // For built-in types, generate the internal type string + let ty_str = format!("{}{array_suffix}", super::ty::TypePrinter::new(cx, base_ty)); + Some(InternalType::Other { contract: None, ty: ty_str }) + } + } +} + fn ty_abi_string(ty: &ast::Type, cx: &ExpCtxt<'_>) -> String { let mut suffix = String::new(); rec_ty_abi_string_suffix(cx, ty, &mut suffix); diff --git a/crates/sol-types/tests/internal_type_test.rs b/crates/sol-types/tests/internal_type_test.rs new file mode 100644 index 000000000..10b8ac869 --- /dev/null +++ b/crates/sol-types/tests/internal_type_test.rs @@ -0,0 +1,306 @@ +//! Tests for internal type generation in ABI. + +#![cfg(feature = "json")] + +use alloy_json_abi::{Error, Event, EventParam, Function, InternalType, Param, StateMutability}; +use alloy_sol_types::sol; + +#[test] +fn test_internal_type_generation() { + sol! { + #[sol(abi)] + contract TestContract { + struct Point { + uint256 x; + uint256 y; + } + + enum Status { + Active, + Inactive + } + + function testFunction( + Point memory p, + Point[] memory points, + Status s + ) external returns (Point memory); + + event TestEvent(Point indexed p, Status s); + + error TestError(Point p, Status[] statuses); + } + } + + let contract_abi = TestContract::abi::contract(); + + let point_struct = Param { + ty: "tuple".into(), + name: "p".into(), + internal_type: Some(InternalType::Struct { + contract: Some("TestContract".into()), + ty: "Point".into(), + }), + components: vec![param("uint256 x"), param("uint256 y")], + }; + + // Test function + assert_eq!( + *contract_abi.function("testFunction").unwrap().first().unwrap(), + Function { + name: "testFunction".into(), + inputs: vec![ + point_struct.clone(), + Param { + ty: "tuple[]".into(), + name: "points".into(), + internal_type: Some(InternalType::Struct { + contract: Some("TestContract".into()), + ty: "Point[]".into(), + }), + components: vec![param("uint256 x"), param("uint256 y")], + }, + Param { + ty: "uint8".into(), + name: "s".into(), + internal_type: Some(InternalType::Enum { + contract: Some("TestContract".into()), + ty: "Status".into(), + }), + components: vec![], + }, + ], + outputs: vec![Param { + ty: "tuple".into(), + name: String::new(), + internal_type: Some(InternalType::Struct { + contract: Some("TestContract".into()), + ty: "Point".into(), + }), + components: vec![param("uint256 x"), param("uint256 y")], + }], + state_mutability: StateMutability::NonPayable, + } + ); + + // Test event + assert_eq!( + *contract_abi.event("TestEvent").unwrap().first().unwrap(), + Event { + name: "TestEvent".into(), + inputs: vec![ + EventParam { + ty: "tuple".into(), + name: "p".into(), + internal_type: Some(InternalType::Struct { + contract: Some("TestContract".into()), + ty: "Point".into(), + }), + components: vec![param("uint256 x"), param("uint256 y")], + indexed: true, + }, + EventParam { + ty: "uint8".into(), + name: "s".into(), + internal_type: Some(InternalType::Enum { + contract: Some("TestContract".into()), + ty: "Status".into(), + }), + components: vec![], + indexed: false, + }, + ], + anonymous: false, + } + ); + + // Test error + assert_eq!( + *contract_abi.error("TestError").unwrap().first().unwrap(), + Error { + name: "TestError".into(), + inputs: vec![ + point_struct, + Param { + ty: "uint8[]".into(), + name: "statuses".into(), + internal_type: Some(InternalType::Enum { + contract: Some("TestContract".into()), + ty: "Status[]".into(), + }), + components: vec![], + }, + ], + } + ); +} + +#[test] +fn test_address_payable_internal_type() { + sol! { + #[sol(abi)] + contract TestPayable { + function testPayable( + address payable recipient, + address payable[] memory recipients + ) external; + } + } + + let contract_abi = TestPayable::abi::contract(); + + assert_eq!( + *contract_abi.function("testPayable").unwrap().first().unwrap(), + Function { + name: "testPayable".into(), + inputs: vec![ + Param { + ty: "address".into(), + name: "recipient".into(), + internal_type: Some(InternalType::AddressPayable("address payable".into())), + components: vec![], + }, + Param { + ty: "address[]".into(), + name: "recipients".into(), + internal_type: Some(InternalType::AddressPayable("address payable[]".into())), + components: vec![], + }, + ], + outputs: vec![], + state_mutability: StateMutability::NonPayable, + } + ); +} + +#[test] +fn test_contract_type_internal_type() { + sol! { + #[sol(abi)] + contract ContractA { + function doSomething() external; + } + + #[sol(abi)] + contract ContractB { + function useContract( + ContractA contractInstance, + ContractA[] memory instances + ) external; + } + } + + let contract_abi = ContractB::abi::contract(); + + assert_eq!( + *contract_abi.function("useContract").unwrap().first().unwrap(), + Function { + name: "useContract".into(), + inputs: vec![ + Param { + ty: "address".into(), + name: "contractInstance".into(), + internal_type: Some(InternalType::Contract("ContractA".into())), + components: vec![], + }, + Param { + ty: "address[]".into(), + name: "instances".into(), + internal_type: Some(InternalType::Contract("ContractA[]".into())), + components: vec![], + }, + ], + outputs: vec![], + state_mutability: StateMutability::NonPayable, + } + ); +} + +#[test] +fn test_udvt_in_nested_struct() { + sol! { + #[sol(abi)] + contract TestUDVT { + type CustomUint is uint128; + type CustomAddress is address; + + struct Inner { + CustomUint amount; + CustomAddress addr; + } + + struct Outer { + Inner inner; + CustomUint[] amounts; + } + + function processNested(Outer memory data) external; + } + } + + let contract_abi = TestUDVT::abi::contract(); + + let inner_struct_components = vec![ + Param { + ty: "uint128".into(), + name: "amount".into(), + internal_type: Some(InternalType::Other { + contract: Some("TestUDVT".into()), + ty: "CustomUint".into(), + }), + components: vec![], + }, + Param { + ty: "address".into(), + name: "addr".into(), + internal_type: Some(InternalType::Other { + contract: Some("TestUDVT".into()), + ty: "CustomAddress".into(), + }), + components: vec![], + }, + ]; + + assert_eq!( + *contract_abi.function("processNested").unwrap().first().unwrap(), + Function { + name: "processNested".into(), + inputs: vec![Param { + ty: "tuple".into(), + name: "data".into(), + internal_type: Some(InternalType::Struct { + contract: Some("TestUDVT".into()), + ty: "Outer".into() + }), + components: vec![ + Param { + ty: "tuple".into(), + name: "inner".into(), + internal_type: Some(InternalType::Struct { + contract: Some("TestUDVT".into()), + ty: "Inner".into() + }), + components: inner_struct_components, + }, + Param { + ty: "uint128[]".into(), + name: "amounts".into(), + internal_type: Some(InternalType::Other { + contract: Some("TestUDVT".into()), + ty: "CustomUint[]".into() + }), + components: vec![], + }, + ], + }], + outputs: vec![], + state_mutability: StateMutability::NonPayable, + } + ); +} + +fn param(s: &str) -> Param { + let (ty, name) = s.split_once(' ').unwrap(); + let internal_type = Some(InternalType::Other { contract: None, ty: ty.to_string() }); + Param { ty: ty.into(), name: name.into(), internal_type, components: vec![] } +} diff --git a/crates/sol-types/tests/macros/sol/abi.rs b/crates/sol-types/tests/macros/sol/abi.rs index a05d92c8a..de09f7a0c 100644 --- a/crates/sol-types/tests/macros/sol/abi.rs +++ b/crates/sol-types/tests/macros/sol/abi.rs @@ -13,6 +13,7 @@ macro_rules! abi_map { #[test] fn equal_abis() { + use alloy_json_abi::InternalType; let contract = Contract::abi::contract(); assert_eq!(contract.constructor, Contract::abi::constructor()); @@ -100,19 +101,28 @@ fn equal_abis() { ty: "tuple".into(), name: String::new(), components: vec![param("uint256 custom")], - internal_type: None, + internal_type: Some(InternalType::Struct { + contract: Some("Contract".into()), + ty: "CustomStruct".into() + }), }, Param { ty: "tuple[]".into(), name: String::new(), components: vec![param("uint256 custom")], - internal_type: None, + internal_type: Some(InternalType::Struct { + contract: Some("Contract".into()), + ty: "CustomStruct[]".into() + }), }, Param { ty: "tuple[][2]".into(), name: String::new(), components: vec![param("uint256 custom")], - internal_type: None, + internal_type: Some(InternalType::Struct { + contract: Some("Contract".into()), + ty: "CustomStruct[][2]".into() + }), }, ], outputs: vec![], @@ -122,10 +132,13 @@ fn equal_abis() { let custom = Param { ty: "tuple".into(), name: "cs".into(), - // TODO: should be `uint256 custom`, but name is lost in recursive resolution - components: vec![param("uint256 ")], - internal_type: None, + components: vec![param("uint256 custom")], + internal_type: Some(InternalType::Struct { + contract: Some("Contract".into()), + ty: "CustomStruct".into(), + }), }; + println!("{custom:#?}"); assert_eq!( *contract.function("F22").unwrap().first().unwrap(), Function { @@ -135,19 +148,28 @@ fn equal_abis() { ty: "tuple".into(), name: String::new(), components: vec![custom.clone(), param("bool cb")], - internal_type: None, + internal_type: Some(InternalType::Struct { + contract: Some("Contract".into()), + ty: "CustomStruct2".into() + }), }, Param { ty: "tuple[]".into(), name: String::new(), components: vec![custom.clone(), param("bool cb")], - internal_type: None, + internal_type: Some(InternalType::Struct { + contract: Some("Contract".into()), + ty: "CustomStruct2[]".into() + }), }, Param { ty: "tuple[][3]".into(), name: String::new(), components: vec![custom, param("bool cb")], - internal_type: None, + internal_type: Some(InternalType::Struct { + contract: Some("Contract".into()), + ty: "CustomStruct2[][3]".into() + }), }, ], outputs: vec![], @@ -244,6 +266,9 @@ fn equal_abis() { } ); + // Verify that contract-scoped and top-level items have identical ABIs + // since they only use primitive types (uint256, bool) with no custom types. + // Custom types would have different internal type qualifiers (contract: Some vs None). macro_rules! eq_modules { ($($items:ident),* $(,)?) => {$( assert_eq!(Contract::$items::abi(), not_contract::$items::abi()); @@ -251,8 +276,68 @@ fn equal_abis() { } eq_modules!( EV00, EV01, EV02, EV10, EV11, EV12, ER0, ER1, ER2, F00Call, F01Call, F02Call, F10Call, - F11Call, F12Call, F20Call, F21Call, F22Call + F11Call, F12Call, F20Call, ); + + // F21Call and F22Call use CustomStruct, so they will differ in internal types: + // Contract-scoped will have contract: Some("Contract"), top-level will have contract: None + // We verify the structure is correct but internal types differ as expected + macro_rules! assert_contract_qualifier_differs { + ($item:ident) => {{ + let contract_item = Contract::$item::abi(); + let toplevel_item = not_contract::$item::abi(); + assert_eq!(contract_item.name, toplevel_item.name); + assert_eq!(contract_item.state_mutability, toplevel_item.state_mutability); + assert_eq!(contract_item.inputs.len(), toplevel_item.inputs.len()); + assert_eq!(contract_item.outputs.len(), toplevel_item.outputs.len()); + + // Helper function to recursively assert params match except for contract qualifiers + fn assert_params_match(c: &Param, t: &Param) { + assert_eq!(c.ty, t.ty); + assert_eq!(c.name, t.name); + assert_eq!(c.components.len(), t.components.len()); + + // Internal types differ: Contract has Some("Contract"), toplevel has None + match (&c.internal_type, &t.internal_type) { + ( + Some(InternalType::Struct { contract: c_contract, ty: c_ty }), + Some(InternalType::Struct { contract: t_contract, ty: t_ty }), + ) => { + assert_eq!(c_contract, &Some("Contract".to_string())); + assert_eq!(t_contract, &None); + assert_eq!(c_ty, t_ty); // Type name is the same + } + (Some(c_int), Some(t_int)) => { + // Other internal types should match exactly + assert_eq!(c_int, t_int); + } + (None, None) => {} + _ => panic!( + "Internal type mismatch: {:?} vs {:?}", + c.internal_type, t.internal_type + ), + } + + // Recursively check components + for (c_comp, t_comp) in c.components.iter().zip(t.components.iter()) { + assert_params_match(c_comp, t_comp); + } + } + + // Input types match but internal type qualifiers differ + for (c, t) in contract_item.inputs.iter().zip(toplevel_item.inputs.iter()) { + assert_params_match(c, t); + } + + // Output types match but internal type qualifiers differ + for (c, t) in contract_item.outputs.iter().zip(toplevel_item.outputs.iter()) { + assert_params_match(c, t); + } + }}; + } + + assert_contract_qualifier_differs!(F21Call); + assert_contract_qualifier_differs!(F22Call); } #[test] @@ -302,27 +387,25 @@ fn recursive() { function stopAndReturnStateDiff() external returns (AccountAccess[] memory accesses); } + use alloy_json_abi::InternalType; let chain_info = Param { ty: "tuple".into(), name: "chainInfo".into(), - components: vec![ - param("uint256 "), // forkId - param("uint256 "), // chainId - ], - internal_type: None, + components: vec![param("uint256 forkId"), param("uint256 chainId")], + internal_type: Some(InternalType::Struct { contract: None, ty: "ChainInfo".into() }), }; 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 + param("address account"), + param("bytes32 slot"), + param("bool isWrite"), + param("bytes32 previousValue"), + param("bytes32 newValue"), + param("bool reverted"), ], - internal_type: None, + internal_type: Some(InternalType::Struct { contract: None, ty: "StorageAccess[]".into() }), }; assert_eq!( stopAndReturnStateDiffCall::abi(), @@ -334,7 +417,15 @@ fn recursive() { name: "accesses".into(), components: vec![ chain_info, - param("uint8 kind"), // TODO: enum + Param { + ty: "uint8".into(), + name: "kind".into(), + components: vec![], + internal_type: Some(InternalType::Enum { + contract: None, + ty: "AccountAccessKind".into() + }), + }, param("address account"), param("address accessor"), param("bool initialized"), @@ -346,7 +437,10 @@ fn recursive() { param("bool reverted"), storage_accesses, ], - internal_type: None, + internal_type: Some(InternalType::Struct { + contract: None, + ty: "AccountAccess[]".into() + }), }], state_mutability: StateMutability::NonPayable, } @@ -391,34 +485,51 @@ fn custom() { ); } + use alloy_json_abi::InternalType; let custom_struct = vec![ param("uint256 custom"), param("uint256[] customArr"), - param("uint32 udvt"), - param("uint32[] udvtArr"), - param("uint8 e"), - param("uint8[] eArr"), - ]; - let custom_struct_erased = vec![ - param("uint256 "), // custom - param("uint256[] "), // customArr - param("uint32 "), // udvt - param("uint32[] "), // udvtArr - param("uint8 "), // e - param("uint8[] "), // eArr + Param { + ty: "uint32".into(), + name: "udvt".into(), + components: vec![], + internal_type: Some(InternalType::Other { contract: None, ty: "UDVT".into() }), + }, + Param { + ty: "uint32[]".into(), + name: "udvtArr".into(), + components: vec![], + internal_type: Some(InternalType::Other { contract: None, ty: "UDVT[]".into() }), + }, + Param { + ty: "uint8".into(), + name: "e".into(), + components: vec![], + internal_type: Some(InternalType::Enum { contract: None, ty: "Enum".into() }), + }, + Param { + ty: "uint8[]".into(), + name: "eArr".into(), + components: vec![], + internal_type: Some(InternalType::Enum { contract: None, ty: "Enum[]".into() }), + }, ]; + let custom_struct_erased = custom_struct.clone(); let custom_struct2 = vec![ Param { ty: "tuple".into(), name: "cs".into(), components: custom_struct_erased.clone(), - internal_type: None, + internal_type: Some(InternalType::Struct { contract: None, ty: "CustomStruct".into() }), }, Param { ty: "tuple[]".into(), name: "csArr".into(), components: custom_struct_erased, - internal_type: None, + internal_type: Some(InternalType::Struct { + contract: None, + ty: "CustomStruct[]".into(), + }), }, ]; assert_eq!( @@ -426,33 +537,68 @@ fn custom() { Function { name: "myFunc".into(), inputs: vec![ - param("uint32 udvt"), - param("uint32[] udvtArr"), - param("uint8 e"), - param("uint8[] eArr"), + Param { + ty: "uint32".into(), + name: "udvt".into(), + components: vec![], + internal_type: Some(InternalType::Other { contract: None, ty: "UDVT".into() }), + }, + Param { + ty: "uint32[]".into(), + name: "udvtArr".into(), + components: vec![], + internal_type: Some(InternalType::Other { + contract: None, + ty: "UDVT[]".into() + }), + }, + Param { + ty: "uint8".into(), + name: "e".into(), + components: vec![], + internal_type: Some(InternalType::Enum { contract: None, ty: "Enum".into() }), + }, + Param { + ty: "uint8[]".into(), + name: "eArr".into(), + components: vec![], + internal_type: Some(InternalType::Enum { contract: None, ty: "Enum[]".into() }), + }, Param { ty: "tuple".into(), name: "cs".into(), components: custom_struct.clone(), - internal_type: None, + internal_type: Some(InternalType::Struct { + contract: None, + ty: "CustomStruct".into() + }), }, Param { ty: "tuple[]".into(), name: "csArr".into(), components: custom_struct, - internal_type: None, + internal_type: Some(InternalType::Struct { + contract: None, + ty: "CustomStruct[]".into() + }), }, Param { ty: "tuple".into(), name: "cs2".into(), components: custom_struct2.clone(), - internal_type: None, + internal_type: Some(InternalType::Struct { + contract: None, + ty: "CustomStruct2".into() + }), }, Param { ty: "tuple[]".into(), name: "cs2Arr".into(), components: custom_struct2, - internal_type: None, + internal_type: Some(InternalType::Struct { + contract: None, + ty: "CustomStruct2[]".into() + }), }, ], outputs: vec![], @@ -548,19 +694,17 @@ mod not_contract { } fn param(s: &str) -> Param { + use alloy_json_abi::InternalType; let (ty, name) = s.split_once(' ').unwrap(); - Param { ty: ty.into(), name: name.into(), internal_type: None, components: vec![] } + let internal_type = Some(InternalType::Other { contract: None, ty: ty.to_string() }); + Param { ty: ty.into(), name: name.into(), internal_type, components: vec![] } } fn eparam(s: &str, indexed: bool) -> EventParam { + use alloy_json_abi::InternalType; let (ty, name) = s.split_once(' ').unwrap(); - EventParam { - ty: ty.into(), - name: name.into(), - internal_type: None, - components: vec![], - indexed, - } + let internal_type = Some(InternalType::Other { contract: None, ty: ty.to_string() }); + EventParam { ty: ty.into(), name: name.into(), internal_type, components: vec![], indexed } } #[test] diff --git a/crates/sol-types/tests/macros/sol/json.rs b/crates/sol-types/tests/macros/sol/json.rs index 9ca56b08d..c2cd4f63c 100644 --- a/crates/sol-types/tests/macros/sol/json.rs +++ b/crates/sol-types/tests/macros/sol/json.rs @@ -33,7 +33,10 @@ fn large_array() { inputs: vec![Param { ty: "uint64[128]".into(), name: "longArray".into(), - internal_type: None, + internal_type: Some(alloy_json_abi::InternalType::Other { + contract: None, + ty: "uint64[128]".into() + }), components: vec![], }], outputs: vec![],