From 42bf98330b2dfc5326389099b251bbb626066410 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 2 Oct 2021 16:34:01 +0200 Subject: [PATCH] feat: support human readable struct inputs (#482) * feat: keep track of custom types in abi parser * feat: use internal structs for abi parsers * test: add human readable struct input test * chore: update changelog * fix conflicts * fix: remove eprintln * make clippy happy * make clippy happy * rustfmt * make clippy happy --- CHANGELOG.md | 4 +- .../ethers-contract-abigen/src/contract.rs | 26 ++++-- .../src/contract/structs.rs | 12 +-- ethers-contract/tests/abigen.rs | 22 ++++- ethers-core/src/abi/human_readable.rs | 91 +++++++++++++++---- 5 files changed, 123 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c21c63d7..987c9958 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Unreleased +* Use rust types as contract function inputs for human readable abi [#482](https://github.com/gakonst/ethers-rs/pull/482) +* ### 0.5.3 * Allow configuring the optimizer & passing arbitrary arguments to solc [#427](https://github.com/gakonst/ethers-rs/pull/427) @@ -44,4 +46,4 @@ ### 0.5.3 -* Added Time Lagged middleware [#457](https://github.com/gakonst/ethers-rs/pull/457) +* Added Time Lagged middleware [#457](https://github.com/gakonst/ethers-rs/pull/457) \ No newline at end of file diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs index c3c0b617..6b29b7a5 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract.rs @@ -131,12 +131,26 @@ impl Context { }; // try to extract all the solidity structs from the normal JSON ABI - // we need to parse the json abi again because we need the internalType fields which are omitted by ethabi. - let internal_structs = (!human_readable) - .then(|| serde_json::from_str::(&abi_str).ok()) - .flatten() - .map(InternalStructs::new) - .unwrap_or_default(); + // we need to parse the json abi again because we need the internalType fields which are omitted by ethabi. If the ABI was defined as human readable we use the `internal_structs` from the Abi Parser + let internal_structs = if human_readable { + let mut internal_structs = InternalStructs::default(); + // the types in the abi_parser are already valid rust types so simply clone them to make it consistent with the `RawAbi` variant + internal_structs.rust_type_names.extend( + abi_parser + .function_params + .values() + .map(|ty| (ty.clone(), ty.clone())), + ); + internal_structs.function_params = abi_parser.function_params.clone(); + internal_structs.outputs = abi_parser.outputs.clone(); + + internal_structs + } else { + serde_json::from_str::(&abi_str) + .ok() + .map(InternalStructs::new) + .unwrap_or_default() + }; let contract_name = util::ident(&args.contract_name); diff --git a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs index 8a1b4a1c..88c683b3 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs @@ -194,22 +194,22 @@ impl Context { #[derive(Debug, Clone, Default)] pub struct InternalStructs { /// All unique internal types that are function inputs or outputs - top_level_internal_types: HashMap, + pub(crate) top_level_internal_types: HashMap, /// (function name, param name) -> struct which are the identifying properties we get the name from ethabi. - function_params: HashMap<(String, String), String>, + pub(crate) function_params: HashMap<(String, String), String>, /// (function name) -> Vec all structs the function returns - outputs: HashMap>, + pub(crate) outputs: HashMap>, /// All the structs extracted from the abi with their identifier as key - structs: HashMap, + pub(crate) structs: HashMap, /// solidity structs as tuples - struct_tuples: HashMap, + pub(crate) struct_tuples: HashMap, /// Contains the names for the rust types (id -> rust type name) - rust_type_names: HashMap, + pub(crate) rust_type_names: HashMap, } impl InternalStructs { diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs index cb577ce4..d79660aa 100644 --- a/ethers-contract/tests/abigen.rs +++ b/ethers-contract/tests/abigen.rs @@ -1,7 +1,9 @@ #![cfg(feature = "abigen")] //! Test cases to validate the `abigen!` macro use ethers_contract::{abigen, EthEvent}; -use ethers_core::abi::Tokenizable; +use ethers_core::abi::{Address, Tokenizable}; +use ethers_providers::Provider; +use std::sync::Arc; #[test] fn can_gen_human_readable() { @@ -74,3 +76,21 @@ fn can_generate_internal_structs() { assert_tokenizeable::(); assert_tokenizeable::(); } + +#[test] +fn can_gen_human_readable_with_structs() { + abigen!( + SimpleContract, + r#"[ + struct Foo { uint256 x; } + function foo(Foo memory x) + ]"#, + event_derives(serde::Deserialize, serde::Serialize) + ); + assert_tokenizeable::(); + + let (client, _mock) = Provider::mocked(); + let contract = SimpleContract::new(Address::default(), Arc::new(client)); + let foo = Foo { x: 100u64.into() }; + let _ = contract.foo(foo); +} diff --git a/ethers-core/src/abi/human_readable.rs b/ethers-core/src/abi/human_readable.rs index 90fa735d..9c013c74 100644 --- a/ethers-core/src/abi/human_readable.rs +++ b/ethers-core/src/abi/human_readable.rs @@ -13,6 +13,10 @@ pub struct AbiParser { pub structs: HashMap, /// solidity structs as tuples pub struct_tuples: HashMap>, + /// (function name, param name) -> struct which are the identifying properties we get the name from ethabi. + pub function_params: HashMap<(String, String), String>, + /// (function name) -> Vec all structs the function returns + pub outputs: HashMap>, } impl AbiParser { @@ -83,7 +87,22 @@ impl AbiParser { .or_default() .push(event); } else if line.starts_with("constructor") { - abi.constructor = Some(self.parse_constructor(line)?); + let inputs = self + .constructor_inputs(line)? + .into_iter() + .map(|(input, struct_name)| { + if let Some(struct_name) = struct_name { + // keep track of the user defined struct of that param + self.function_params.insert( + ("constructor".to_string(), input.name.clone()), + struct_name, + ); + } + input + }) + .collect(); + + abi.constructor = Some(Constructor { inputs }); } else { // function may have shorthand declaration, so it won't start with "function" let function = match self.parse_function(line) { @@ -148,6 +167,8 @@ impl AbiParser { .map(|s| (s.name().to_string(), s)) .collect(), struct_tuples: HashMap::new(), + function_params: Default::default(), + outputs: Default::default(), } } @@ -233,7 +254,7 @@ impl AbiParser { Ok(EventParam { name: name.to_string(), indexed, - kind: self.parse_type(type_str)?, + kind: self.parse_type(type_str)?.0, }) } @@ -278,6 +299,16 @@ impl AbiParser { let inputs = if let Some(params) = input_args { self.parse_params(params)? + .into_iter() + .map(|(input, struct_name)| { + if let Some(struct_name) = struct_name { + // keep track of the user defined struct of that param + self.function_params + .insert((name.clone(), input.name.clone()), struct_name); + } + input + }) + .collect() } else { Vec::new() }; @@ -287,7 +318,19 @@ impl AbiParser { .trim() .strip_suffix(')') .ok_or_else(|| format_err!("Expected output args parentheses at `{}`", s))?; - self.parse_params(params)? + let output_params = self.parse_params(params)?; + let mut outputs = Vec::with_capacity(output_params.len()); + let mut output_types = Vec::new(); + + for (output, struct_name) in output_params { + if let Some(struct_name) = struct_name { + // keep track of the user defined struct of that param + output_types.push(struct_name); + } + outputs.push(output); + } + self.outputs.insert(name.clone(), output_types); + outputs } else { Vec::new() }; @@ -304,31 +347,33 @@ impl AbiParser { }) } - fn parse_params(&self, s: &str) -> Result> { + fn parse_params(&self, s: &str) -> Result)>> { s.split(',') .filter(|s| !s.is_empty()) .map(|s| self.parse_param(s)) .collect::, _>>() } - fn parse_type(&self, type_str: &str) -> Result { + /// Returns the `ethabi` `ParamType` for the function parameter and the aliased struct type, if it is a user defined struct + fn parse_type(&self, type_str: &str) -> Result<(ParamType, Option)> { if let Ok(kind) = Reader::read(type_str) { - Ok(kind) + Ok((kind, None)) } else { // try struct instead if let Ok(field) = StructFieldType::parse(type_str) { let struct_ty = field .as_struct() .ok_or_else(|| format_err!("Expected struct type `{}`", type_str))?; + let name = struct_ty.name(); let tuple = self .struct_tuples - .get(struct_ty.name()) + .get(name) .cloned() .map(ParamType::Tuple) .ok_or_else(|| format_err!("Unknown struct `{}`", struct_ty.name()))?; if let Some(field) = field.as_struct() { - Ok(field.as_param(tuple)) + Ok((field.as_param(tuple), Some(name.to_string()))) } else { bail!("Expected struct type") } @@ -339,6 +384,15 @@ impl AbiParser { } pub fn parse_constructor(&self, s: &str) -> Result { + let inputs = self + .constructor_inputs(s)? + .into_iter() + .map(|s| s.0) + .collect(); + Ok(Constructor { inputs }) + } + + fn constructor_inputs(&self, s: &str) -> Result)>> { let mut input = s.trim(); if !input.starts_with("constructor") { bail!("Not a constructor `{}`", input) @@ -353,12 +407,10 @@ impl AbiParser { .last() .ok_or_else(|| format_err!("Expected closing `)` in `{}`", s))?; - let inputs = self.parse_params(params)?; - - Ok(Constructor { inputs }) + self.parse_params(params) } - fn parse_param(&self, param: &str) -> Result { + fn parse_param(&self, param: &str) -> Result<(Param, Option)> { let mut iter = param.trim().rsplitn(3, is_whitespace); let mut name = iter @@ -375,12 +427,15 @@ impl AbiParser { type_str = name; name = ""; } - - Ok(Param { - name: name.to_string(), - kind: self.parse_type(type_str)?, - internal_type: None, - }) + let (kind, user_struct) = self.parse_type(type_str)?; + Ok(( + Param { + name: name.to_string(), + kind, + internal_type: None, + }, + user_struct, + )) } }