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
|
### 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)
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue