From 59cf9918289012d9235e70da40f8d93c1f9ff692 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Fri, 10 Dec 2021 00:00:59 +0100 Subject: [PATCH] feat(abigen): use structs for outputs (#664) * feat(abigen): use structs for outputs * update CHANGELOG * make clippy happy * fix lints --- CHANGELOG.md | 2 + .../src/contract/events.rs | 3 +- .../src/contract/methods.rs | 112 +++++++++++++----- .../src/contract/structs.rs | 16 +++ ethers-contract/src/multicall/mod.rs | 2 +- ethers-contract/tests/abigen.rs | 29 +++++ .../solidity-contracts/Abiencoderv2Test.sol | 12 ++ .../abiencoderv2test_abi.json | 1 + ethers-etherscan/src/gas.rs | 2 +- ethers-signers/src/ledger/app.rs | 2 +- ethers-solc/src/compile.rs | 2 +- 11 files changed, 147 insertions(+), 36 deletions(-) create mode 100644 ethers-contract/tests/solidity-contracts/Abiencoderv2Test.sol create mode 100644 ethers-contract/tests/solidity-contracts/abiencoderv2test_abi.json diff --git a/CHANGELOG.md b/CHANGELOG.md index b7d268b4..f5538665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,8 @@ ### Unreleased +- Substitute output tuples with rust struct types for function calls + [#664](https://github.com/gakonst/ethers-rs/pull/664) - Add AbiType implementation during EthAbiType expansion [#647](https://github.com/gakonst/ethers-rs/pull/647) - fix Etherscan conditional HTTP support diff --git a/ethers-contract/ethers-contract-abigen/src/contract/events.rs b/ethers-contract/ethers-contract-abigen/src/contract/events.rs index f48eabc4..64667cf1 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/events.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/events.rs @@ -38,8 +38,7 @@ impl Context { let sorted_events: BTreeMap<_, _> = self.abi.events.iter().collect(); let filter_methods = sorted_events .values() - .map(std::ops::Deref::deref) - .flatten() + .flat_map(std::ops::Deref::deref) .map(|event| self.expand_filter(event)) .collect::>(); diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs index eb40aa8f..7827eba2 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs @@ -28,8 +28,7 @@ impl Context { let sorted_functions: BTreeMap<_, _> = self.abi.functions.iter().collect(); let functions = sorted_functions .values() - .map(std::ops::Deref::deref) - .flatten() + .flat_map(std::ops::Deref::deref) .map(|function| { let signature = function.abi_signature(); self.expand_function(function, aliases.get(&signature).cloned()) @@ -50,7 +49,7 @@ impl Context { alias: Option<&MethodAlias>, ) -> Result { let call_name = expand_call_struct_name(function, alias); - let fields = self.expand_input_pairs(function)?; + let fields = self.expand_input_params(function)?; // expand as a tuple if all fields are anonymous let all_anonymous_fields = function.inputs.iter().all(|input| input.name.is_empty()); let call_type_definition = if all_anonymous_fields { @@ -163,16 +162,34 @@ impl Context { } /// Expands to the `name : type` pairs of the function's inputs - fn expand_input_pairs(&self, fun: &Function) -> Result> { + fn expand_input_params(&self, fun: &Function) -> Result> { let mut args = Vec::with_capacity(fun.inputs.len()); for (idx, param) in fun.inputs.iter().enumerate() { let name = util::expand_input_name(idx, ¶m.name); - let ty = self.expand_input_param(fun, ¶m.name, ¶m.kind)?; + let ty = self.expand_input_param_type(fun, ¶m.name, ¶m.kind)?; args.push((name, ty)); } Ok(args) } + /// Expands to the return type of a function + fn expand_outputs(&self, fun: &Function) -> Result { + let mut outputs = Vec::with_capacity(fun.outputs.len()); + for param in fun.outputs.iter() { + let ty = self.expand_output_param_type(fun, param, ¶m.kind)?; + outputs.push(ty); + } + + let return_ty = match outputs.len() { + 0 => quote! { () }, + 1 => outputs[0].clone(), + _ => { + quote! { (#( #outputs ),*) } + } + }; + Ok(return_ty) + } + /// Expands the arguments for the call that eventually calls the contract fn expand_contract_call_args(&self, fun: &Function) -> Result { let mut call_args = Vec::with_capacity(fun.inputs.len()); @@ -202,7 +219,8 @@ impl Context { Ok(call_args) } - fn expand_input_param( + /// returns the Tokenstream for the corresponding rust type of the param + fn expand_input_param_type( &self, fun: &Function, param: &str, @@ -210,13 +228,13 @@ impl Context { ) -> Result { match kind { ParamType::Array(ty) => { - let ty = self.expand_input_param(fun, param, ty)?; + let ty = self.expand_input_param_type(fun, param, ty)?; Ok(quote! { ::std::vec::Vec<#ty> }) } ParamType::FixedArray(ty, size) => { - let ty = self.expand_input_param(fun, param, ty)?; + let ty = self.expand_input_param_type(fun, param, ty)?; let size = *size; Ok(quote! {[#ty; #size]}) } @@ -235,27 +253,61 @@ impl Context { } } + /// returns the Tokenstream for the corresponding rust type of the output param + fn expand_output_param_type( + &self, + fun: &Function, + param: &Param, + kind: &ParamType, + ) -> Result { + match kind { + ParamType::Array(ty) => { + let ty = self.expand_output_param_type(fun, param, ty)?; + Ok(quote! { + ::std::vec::Vec<#ty> + }) + } + ParamType::FixedArray(ty, size) => { + let ty = self.expand_output_param_type(fun, param, ty)?; + let size = *size; + Ok(quote! {[#ty; #size]}) + } + ParamType::Tuple(_) => { + let ty = if let Some(rust_struct_name) = + param.internal_type.as_ref().and_then(|s| { + self.internal_structs.get_function_output_struct_type(&fun.name, s) + }) { + let ident = util::ident(rust_struct_name); + quote! {#ident} + } else { + types::expand(kind)? + }; + Ok(ty) + } + _ => types::expand(kind), + } + } + /// Expands a single function with the given alias fn expand_function( &self, function: &Function, alias: Option, ) -> Result { + let ethers_contract = ethers_contract_crate(); + let name = expand_function_name(function, alias.as_ref()); let selector = expand_selector(function.selector()); - // TODO use structs - let outputs = expand_fn_outputs(&function.outputs)?; - - let ethers_contract = ethers_contract_crate(); - - let result = quote! { #ethers_contract::builders::ContractCall }; - let contract_args = self.expand_contract_call_args(function)?; let function_params = - self.expand_input_pairs(function)?.into_iter().map(|(name, ty)| quote! { #name: #ty }); + self.expand_input_params(function)?.into_iter().map(|(name, ty)| quote! { #name: #ty }); let function_params = quote! { #( , #function_params )* }; + let outputs = self.expand_outputs(function)?; + + let result = quote! { #ethers_contract::builders::ContractCall }; + let doc = util::expand_doc(&format!( "Calls the contract's `{}` (0x{}) function", function.name, @@ -478,20 +530,6 @@ impl Context { } } -fn expand_fn_outputs(outputs: &[Param]) -> Result { - match outputs.len() { - 0 => Ok(quote! { () }), - 1 => types::expand(&outputs[0].kind), - _ => { - let types = outputs - .iter() - .map(|param| types::expand(¶m.kind)) - .collect::>>()?; - Ok(quote! { (#( #types ),*) }) - } - } -} - fn expand_selector(selector: Selector) -> TokenStream { let bytes = selector.iter().copied().map(Literal::u8_unsuffixed); quote! { [#( #bytes ),*] } @@ -575,6 +613,20 @@ mod tests { use super::*; + fn expand_fn_outputs(outputs: &[Param]) -> Result { + match outputs.len() { + 0 => Ok(quote! { () }), + 1 => types::expand(&outputs[0].kind), + _ => { + let types = outputs + .iter() + .map(|param| types::expand(¶m.kind)) + .collect::>>()?; + Ok(quote! { (#( #types ),*) }) + } + } + } + // packs the argument in a tuple to be used for the contract call fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream { let names = inputs diff --git a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs index 526a8dcf..64a90bcd 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs @@ -312,6 +312,22 @@ impl InternalStructs { .map(String::as_str) } + /// Returns the name of the rust type that will be generated if the given output is a struct + /// NOTE: this does not account for arrays or fixed arrays + pub fn get_function_output_struct_type( + &self, + function: &str, + internal_type: &str, + ) -> Option<&str> { + self.outputs + .get(function) + .and_then(|outputs| { + outputs.iter().find(|s| s.as_str() == struct_type_identifier(internal_type)) + }) + .and_then(|id| self.rust_type_names.get(id)) + .map(String::as_str) + } + /// Returns the mapping table of abi `internal type identifier -> rust type` pub fn rust_type_names(&self) -> &HashMap { &self.rust_type_names diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs index 096969d6..ebc4225d 100644 --- a/ethers-contract/src/multicall/mod.rs +++ b/ethers-contract/src/multicall/mod.rs @@ -191,7 +191,7 @@ impl Multicall { /// If more than the maximum number of supported calls are added. The maximum /// limits is constrained due to tokenization/detokenization support for tuples pub fn add_call(&mut self, call: ContractCall) -> &mut Self { - assert!(!(self.calls.len() >= 16), "Cannot support more than {} calls", 16); + assert!(self.calls.len() < 16, "Cannot support more than {} calls", 16); match (call.tx.to(), call.tx.data()) { (Some(NameOrAddress::Address(target)), Some(data)) => { diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs index b49c83bb..f3e7b4e9 100644 --- a/ethers-contract/tests/abigen.rs +++ b/ethers-contract/tests/abigen.rs @@ -419,3 +419,32 @@ fn can_handle_case_sensitive_calls() { let _ = contract.index(); let _ = contract.INDEX(); } + +#[tokio::test] +async fn can_abiencoderv2_output() { + abigen!(AbiEncoderv2Test, "ethers-contract/tests/solidity-contracts/abiencoderv2test_abi.json",); + let ganache = ethers_core::utils::Ganache::new().spawn(); + let from = ganache.addresses()[0]; + let provider = Provider::try_from(ganache.endpoint()) + .unwrap() + .with_sender(from) + .interval(std::time::Duration::from_millis(10)); + let client = Arc::new(provider); + + let contract = "AbiencoderV2Test"; + let path = "./tests/solidity-contracts/Abiencoderv2Test.sol"; + let compiled = Solc::default().compile_source(path).unwrap(); + let compiled = compiled.get(path, contract).unwrap(); + let factory = ethers_contract::ContractFactory::new( + compiled.abi.unwrap().clone(), + compiled.bytecode().unwrap().clone(), + client.clone(), + ); + let addr = factory.deploy(()).unwrap().legacy().send().await.unwrap().address(); + + let contract = AbiEncoderv2Test::new(addr, client.clone()); + let person = Person { name: "Alice".to_string(), age: 20u64.into() }; + + let res = contract.default_person().call().await.unwrap(); + assert_eq!(res, person); +} diff --git a/ethers-contract/tests/solidity-contracts/Abiencoderv2Test.sol b/ethers-contract/tests/solidity-contracts/Abiencoderv2Test.sol new file mode 100644 index 00000000..a89fcf02 --- /dev/null +++ b/ethers-contract/tests/solidity-contracts/Abiencoderv2Test.sol @@ -0,0 +1,12 @@ +pragma solidity >=0.6.0; +pragma experimental ABIEncoderV2; + +contract AbiencoderV2Test { + struct Person { + string name; + uint age; + } + function defaultPerson() public pure returns (Person memory) { + return Person("Alice", 20); + } +} \ No newline at end of file diff --git a/ethers-contract/tests/solidity-contracts/abiencoderv2test_abi.json b/ethers-contract/tests/solidity-contracts/abiencoderv2test_abi.json new file mode 100644 index 00000000..13430b68 --- /dev/null +++ b/ethers-contract/tests/solidity-contracts/abiencoderv2test_abi.json @@ -0,0 +1 @@ +[{"inputs":[],"name":"defaultPerson","outputs":[{"components":[{"internalType":"string","name":"name","type":"string"},{"internalType":"uint256","name":"age","type":"uint256"}],"internalType":"struct Hello.Person","name":"","type":"tuple"}],"stateMutability":"pure","type":"function"}] \ No newline at end of file diff --git a/ethers-etherscan/src/gas.rs b/ethers-etherscan/src/gas.rs index bdbbc640..e9c7e1de 100644 --- a/ethers-etherscan/src/gas.rs +++ b/ethers-etherscan/src/gas.rs @@ -123,7 +123,7 @@ mod tests { assert!(oracle.fast_gas_price > 0); assert!(oracle.last_block > 0); assert!(oracle.suggested_base_fee > 0.0); - assert!(oracle.gas_used_ratio.len() > 0); + assert!(!oracle.gas_used_ratio.is_empty()); }) .await } diff --git a/ethers-signers/src/ledger/app.rs b/ethers-signers/src/ledger/app.rs index 31e20d06..558d0995 100644 --- a/ethers-signers/src/ledger/app.rs +++ b/ethers-signers/src/ledger/app.rs @@ -206,7 +206,7 @@ impl LedgerEthereum { let mut bytes = vec![depth as u8]; for derivation_index in elements { let hardened = derivation_index.contains('\''); - let mut index = derivation_index.replace("'", "").parse::().unwrap(); + let mut index = derivation_index.replace('\'', "").parse::().unwrap(); if hardened { index |= 0x80000000; } diff --git a/ethers-solc/src/compile.rs b/ethers-solc/src/compile.rs index d52726da..73246444 100644 --- a/ethers-solc/src/compile.rs +++ b/ethers-solc/src/compile.rs @@ -202,7 +202,7 @@ impl Solc { pub fn version_req(source: &Source) -> Result { let version = utils::find_version_pragma(&source.content) .ok_or(SolcError::PragmaNotFound)? - .replace(" ", ","); + .replace(' ', ","); // Somehow, Solidity semver without an operator is considered to be "exact", // but lack of operator automatically marks the operator as Caret, so we need