Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support human readable struct inputs #482

Merged
merged 11 commits into from
Oct 2, 2021
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
26 changes: 20 additions & 6 deletions ethers-contract/ethers-contract-abigen/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
12 changes: 6 additions & 6 deletions ethers-contract/ethers-contract-abigen/src/contract/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
22 changes: 21 additions & 1 deletion ethers-contract/tests/abigen.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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);
}
91 changes: 73 additions & 18 deletions ethers-core/src/abi/human_readable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(),
}
}

Expand Down Expand Up @@ -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,
})
}

Expand Down Expand Up @@ -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()
};
Expand All @@ -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()
};
Expand All @@ -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")
}
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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,
))
}
}

Expand Down