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
* 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)

View File

@ -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::<RawAbi>(&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::<RawAbi>(&abi_str)
.ok()
.map(InternalStructs::new)
.unwrap_or_default()
};
let contract_name = util::ident(&args.contract_name);

View File

@ -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<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_params: HashMap<(String, String), String>,
pub(crate) function_params: HashMap<(String, String), String>,
/// (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
structs: HashMap<String, SolStruct>,
pub(crate) structs: HashMap<String, SolStruct>,
/// 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)
rust_type_names: HashMap<String, String>,
pub(crate) rust_type_names: HashMap<String, String>,
}
impl InternalStructs {

View File

@ -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::<G1Point>();
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>,
/// solidity structs as tuples
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 {
@ -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<Vec<Param>> {
fn parse_params(&self, s: &str) -> Result<Vec<(Param, Option<String>)>> {
s.split(',')
.filter(|s| !s.is_empty())
.map(|s| self.parse_param(s))
.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) {
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<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();
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<Param> {
fn parse_param(&self, param: &str) -> Result<(Param, Option<String>)> {
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,
))
}
}