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:
parent
da70e0caab
commit
42bf98330b
|
@ -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)
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue