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
This commit is contained in:
Matthias Seitz 2021-10-02 16:34:01 +02:00 committed by GitHub
parent da70e0caab
commit 42bf98330b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 123 additions and 32 deletions

View File

@ -4,6 +4,8 @@
### Unreleased ### Unreleased
* Use rust types as contract function inputs for human readable abi [#482](https://github.com/gakonst/ethers-rs/pull/482)
*
### 0.5.3 ### 0.5.3
* Allow configuring the optimizer & passing arbitrary arguments to solc [#427](https://github.com/gakonst/ethers-rs/pull/427) * 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 ### 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)

View File

@ -131,12 +131,26 @@ impl Context {
}; };
// try to extract all the solidity structs from the normal JSON ABI // 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. // 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 = (!human_readable) let internal_structs = if human_readable {
.then(|| serde_json::from_str::<RawAbi>(&abi_str).ok()) let mut internal_structs = InternalStructs::default();
.flatten() // the types in the abi_parser are already valid rust types so simply clone them to make it consistent with the `RawAbi` variant
.map(InternalStructs::new) internal_structs.rust_type_names.extend(
.unwrap_or_default(); 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::<RawAbi>(&abi_str)
.ok()
.map(InternalStructs::new)
.unwrap_or_default()
};
let contract_name = util::ident(&args.contract_name); let contract_name = util::ident(&args.contract_name);

View File

@ -194,22 +194,22 @@ impl Context {
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct InternalStructs { pub struct InternalStructs {
/// All unique internal types that are function inputs or outputs /// All unique internal types that are function inputs or outputs
top_level_internal_types: HashMap<String, Component>, pub(crate) top_level_internal_types: HashMap<String, Component>,
/// (function name, param name) -> struct which are the identifying properties we get the name from ethabi. /// (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<structs> all structs the function returns /// (function name) -> Vec<structs> all structs the function returns
outputs: HashMap<String, Vec<String>>, pub(crate) outputs: HashMap<String, Vec<String>>,
/// All the structs extracted from the abi with their identifier as key /// All the structs extracted from the abi with their identifier as key
structs: HashMap<String, SolStruct>, pub(crate) structs: HashMap<String, SolStruct>,
/// solidity structs as tuples /// solidity structs as tuples
struct_tuples: HashMap<String, ParamType>, pub(crate) struct_tuples: HashMap<String, ParamType>,
/// Contains the names for the rust types (id -> rust type name) /// Contains the names for the rust types (id -> rust type name)
rust_type_names: HashMap<String, String>, pub(crate) rust_type_names: HashMap<String, String>,
} }
impl InternalStructs { impl InternalStructs {

View File

@ -1,7 +1,9 @@
#![cfg(feature = "abigen")] #![cfg(feature = "abigen")]
//! Test cases to validate the `abigen!` macro //! Test cases to validate the `abigen!` macro
use ethers_contract::{abigen, EthEvent}; 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] #[test]
fn can_gen_human_readable() { fn can_gen_human_readable() {
@ -74,3 +76,21 @@ fn can_generate_internal_structs() {
assert_tokenizeable::<G1Point>(); assert_tokenizeable::<G1Point>();
assert_tokenizeable::<G2Point>(); assert_tokenizeable::<G2Point>();
} }
#[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::<Foo>();
let (client, _mock) = Provider::mocked();
let contract = SimpleContract::new(Address::default(), Arc::new(client));
let foo = Foo { x: 100u64.into() };
let _ = contract.foo(foo);
}

View File

@ -13,6 +13,10 @@ pub struct AbiParser {
pub structs: HashMap<String, SolStruct>, pub structs: HashMap<String, SolStruct>,
/// solidity structs as tuples /// solidity structs as tuples
pub struct_tuples: HashMap<String, Vec<ParamType>>, pub struct_tuples: HashMap<String, Vec<ParamType>>,
/// (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<structs> all structs the function returns
pub outputs: HashMap<String, Vec<String>>,
} }
impl AbiParser { impl AbiParser {
@ -83,7 +87,22 @@ impl AbiParser {
.or_default() .or_default()
.push(event); .push(event);
} else if line.starts_with("constructor") { } 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 { } else {
// function may have shorthand declaration, so it won't start with "function" // function may have shorthand declaration, so it won't start with "function"
let function = match self.parse_function(line) { let function = match self.parse_function(line) {
@ -148,6 +167,8 @@ impl AbiParser {
.map(|s| (s.name().to_string(), s)) .map(|s| (s.name().to_string(), s))
.collect(), .collect(),
struct_tuples: HashMap::new(), struct_tuples: HashMap::new(),
function_params: Default::default(),
outputs: Default::default(),
} }
} }
@ -233,7 +254,7 @@ impl AbiParser {
Ok(EventParam { Ok(EventParam {
name: name.to_string(), name: name.to_string(),
indexed, 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 { let inputs = if let Some(params) = input_args {
self.parse_params(params)? 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 { } else {
Vec::new() Vec::new()
}; };
@ -287,7 +318,19 @@ impl AbiParser {
.trim() .trim()
.strip_suffix(')') .strip_suffix(')')
.ok_or_else(|| format_err!("Expected output args parentheses at `{}`", s))?; .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 { } else {
Vec::new() Vec::new()
}; };
@ -304,31 +347,33 @@ impl AbiParser {
}) })
} }
fn parse_params(&self, s: &str) -> Result<Vec<Param>> { fn parse_params(&self, s: &str) -> Result<Vec<(Param, Option<String>)>> {
s.split(',') s.split(',')
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.map(|s| self.parse_param(s)) .map(|s| self.parse_param(s))
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
} }
fn parse_type(&self, type_str: &str) -> Result<ParamType> { /// 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<String>)> {
if let Ok(kind) = Reader::read(type_str) { if let Ok(kind) = Reader::read(type_str) {
Ok(kind) Ok((kind, None))
} else { } else {
// try struct instead // try struct instead
if let Ok(field) = StructFieldType::parse(type_str) { if let Ok(field) = StructFieldType::parse(type_str) {
let struct_ty = field let struct_ty = field
.as_struct() .as_struct()
.ok_or_else(|| format_err!("Expected struct type `{}`", type_str))?; .ok_or_else(|| format_err!("Expected struct type `{}`", type_str))?;
let name = struct_ty.name();
let tuple = self let tuple = self
.struct_tuples .struct_tuples
.get(struct_ty.name()) .get(name)
.cloned() .cloned()
.map(ParamType::Tuple) .map(ParamType::Tuple)
.ok_or_else(|| format_err!("Unknown struct `{}`", struct_ty.name()))?; .ok_or_else(|| format_err!("Unknown struct `{}`", struct_ty.name()))?;
if let Some(field) = field.as_struct() { if let Some(field) = field.as_struct() {
Ok(field.as_param(tuple)) Ok((field.as_param(tuple), Some(name.to_string())))
} else { } else {
bail!("Expected struct type") bail!("Expected struct type")
} }
@ -339,6 +384,15 @@ impl AbiParser {
} }
pub fn parse_constructor(&self, s: &str) -> Result<Constructor> { pub fn parse_constructor(&self, s: &str) -> Result<Constructor> {
let inputs = self
.constructor_inputs(s)?
.into_iter()
.map(|s| s.0)
.collect();
Ok(Constructor { inputs })
}
fn constructor_inputs(&self, s: &str) -> Result<Vec<(Param, Option<String>)>> {
let mut input = s.trim(); let mut input = s.trim();
if !input.starts_with("constructor") { if !input.starts_with("constructor") {
bail!("Not a constructor `{}`", input) bail!("Not a constructor `{}`", input)
@ -353,12 +407,10 @@ impl AbiParser {
.last() .last()
.ok_or_else(|| format_err!("Expected closing `)` in `{}`", s))?; .ok_or_else(|| format_err!("Expected closing `)` in `{}`", s))?;
let inputs = self.parse_params(params)?; self.parse_params(params)
Ok(Constructor { inputs })
} }
fn parse_param(&self, param: &str) -> Result<Param> { fn parse_param(&self, param: &str) -> Result<(Param, Option<String>)> {
let mut iter = param.trim().rsplitn(3, is_whitespace); let mut iter = param.trim().rsplitn(3, is_whitespace);
let mut name = iter let mut name = iter
@ -375,12 +427,15 @@ impl AbiParser {
type_str = name; type_str = name;
name = ""; name = "";
} }
let (kind, user_struct) = self.parse_type(type_str)?;
Ok(Param { Ok((
name: name.to_string(), Param {
kind: self.parse_type(type_str)?, name: name.to_string(),
internal_type: None, kind,
}) internal_type: None,
},
user_struct,
))
} }
} }