feat(abigen): subsitute structs in event bindings (#1674)
* fix(abigen): handle event defaults * feat(abigen): subsitute structs in event bindings * update changelog * chore: rustfmt * fix broken tests * chore(clippy): make clippy happy
This commit is contained in:
parent
8c3e9985d5
commit
0e7f46b03d
|
@ -93,6 +93,7 @@
|
|||
|
||||
### Unreleased
|
||||
|
||||
- Use corresponding rust structs for event fields if they're solidity structs [#1674](https://github.com/gakonst/ethers-rs/pull/1674)
|
||||
- Add `ContractFilter` to filter contracts in `MultiAbigen` [#1564](https://github.com/gakonst/ethers-rs/pull/1564)
|
||||
- generate error bindings for custom errors [#1549](https://github.com/gakonst/ethers-rs/pull/1549)
|
||||
- Support overloaded events
|
||||
|
|
|
@ -207,6 +207,7 @@ impl Context {
|
|||
.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.event_params = abi_parser.event_params.clone();
|
||||
internal_structs.outputs = abi_parser.outputs.clone();
|
||||
|
||||
internal_structs
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::{types, util, Context};
|
||||
use crate::util::can_derive_defaults;
|
||||
use ethers_core::{
|
||||
abi::{Event, EventExt, EventParam, ParamType, SolStruct},
|
||||
abi::{Event, EventExt, EventParam, Param, ParamType},
|
||||
macros::{ethers_contract_crate, ethers_core_crate},
|
||||
};
|
||||
use eyre::Result;
|
||||
|
@ -140,55 +141,60 @@ impl Context {
|
|||
/// Note that this is slightly different from expanding a Solidity type as
|
||||
/// complex types like arrays and strings get emitted as hashes when they are
|
||||
/// indexed.
|
||||
/// If a complex types matches with a struct previously parsed by the AbiParser,
|
||||
/// If a complex types matches with a struct previously parsed by the internal structs,
|
||||
/// we can replace it
|
||||
fn expand_input_type(&self, input: &EventParam) -> Result<TokenStream> {
|
||||
fn expand_input_type(
|
||||
&self,
|
||||
event: &Event,
|
||||
input: &EventParam,
|
||||
idx: usize,
|
||||
) -> Result<TokenStream> {
|
||||
let ethers_core = ethers_core_crate();
|
||||
Ok(match (&input.kind, input.indexed) {
|
||||
(ParamType::Array(ty), true) => {
|
||||
if let ParamType::Tuple(..) = **ty {
|
||||
// represents an array of a struct
|
||||
if let Some(ty) = self
|
||||
.abi_parser
|
||||
.structs
|
||||
.get(&input.name)
|
||||
.map(SolStruct::name)
|
||||
.map(util::ident)
|
||||
{
|
||||
return Ok(quote! {::std::vec::Vec<#ty>})
|
||||
}
|
||||
}
|
||||
(ParamType::Array(_), true) => {
|
||||
quote! { #ethers_core::types::H256 }
|
||||
}
|
||||
(ParamType::FixedArray(ty, size), true) => {
|
||||
if let ParamType::Tuple(..) = **ty {
|
||||
// represents a fixed array of a struct
|
||||
if let Some(ty) = self
|
||||
.abi_parser
|
||||
.structs
|
||||
.get(&input.name)
|
||||
.map(SolStruct::name)
|
||||
.map(util::ident)
|
||||
{
|
||||
let size = Literal::usize_unsuffixed(*size);
|
||||
return Ok(quote! {[#ty; #size]})
|
||||
}
|
||||
}
|
||||
(ParamType::FixedArray(_, _), true) => {
|
||||
quote! { #ethers_core::types::H256 }
|
||||
}
|
||||
(ParamType::Tuple(..), true) => {
|
||||
// represents a struct
|
||||
if let Some(ty) =
|
||||
self.abi_parser.structs.get(&input.name).map(SolStruct::name).map(util::ident)
|
||||
{
|
||||
quote! {#ty}
|
||||
} else {
|
||||
quote! { #ethers_core::types::H256 }
|
||||
}
|
||||
}
|
||||
(ParamType::Bytes, true) | (ParamType::String, true) => {
|
||||
quote! { #ethers_core::types::H256 }
|
||||
}
|
||||
(ParamType::Tuple(_), false) => {
|
||||
let ty = if let Some(rust_struct_name) =
|
||||
self.internal_structs.get_event_input_struct_type(&event.name, idx)
|
||||
{
|
||||
let ident = util::ident(rust_struct_name);
|
||||
quote! {#ident}
|
||||
} else {
|
||||
types::expand(&input.kind)?
|
||||
};
|
||||
ty
|
||||
}
|
||||
(ParamType::Array(_), _) => {
|
||||
// represents an array of a struct
|
||||
if let Some(rust_struct_name) =
|
||||
self.internal_structs.get_event_input_struct_type(&event.name, idx)
|
||||
{
|
||||
let ty = util::ident(rust_struct_name);
|
||||
return Ok(quote! {::std::vec::Vec<#ty>})
|
||||
}
|
||||
types::expand(&input.kind)?
|
||||
}
|
||||
(ParamType::FixedArray(_, size), _) => {
|
||||
// represents a fixed array of a struct
|
||||
if let Some(rust_struct_name) =
|
||||
self.internal_structs.get_event_input_struct_type(&event.name, idx)
|
||||
{
|
||||
let ty = util::ident(rust_struct_name);
|
||||
let size = Literal::usize_unsuffixed(*size);
|
||||
return Ok(quote! {[#ty; #size]})
|
||||
}
|
||||
types::expand(&input.kind)?
|
||||
}
|
||||
(kind, _) => types::expand(kind)?,
|
||||
})
|
||||
}
|
||||
|
@ -199,10 +205,10 @@ impl Context {
|
|||
.inputs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, input)| {
|
||||
.map(|(idx, input)| {
|
||||
// NOTE: Events can contain nameless values.
|
||||
let name = util::expand_input_name(i, &input.name);
|
||||
let ty = self.expand_input_type(input)?;
|
||||
let name = util::expand_input_name(idx, &input.name);
|
||||
let ty = self.expand_input_type(event, input, idx)?;
|
||||
|
||||
Ok((name, ty, input.indexed))
|
||||
})
|
||||
|
@ -255,10 +261,31 @@ impl Context {
|
|||
|
||||
let derives = util::expand_derives(&self.event_derives);
|
||||
|
||||
// rust-std only derives default automatically for arrays len <= 32
|
||||
// for large array types we skip derive(Default) <https://github.com/gakonst/ethers-rs/issues/1640>
|
||||
let derive_default = if can_derive_defaults(
|
||||
&event
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|param| Param {
|
||||
name: param.name.clone(),
|
||||
kind: param.kind.clone(),
|
||||
internal_type: None,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
) {
|
||||
quote! {
|
||||
#[derive(Default)]
|
||||
}
|
||||
} else {
|
||||
quote! {}
|
||||
};
|
||||
|
||||
let ethers_contract = ethers_contract_crate();
|
||||
|
||||
Ok(quote! {
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthEvent, #ethers_contract::EthDisplay, #derives)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, #ethers_contract::EthEvent, #ethers_contract::EthDisplay, #derives)]
|
||||
#derive_default
|
||||
#[ethevent( name = #event_abi_name, abi = #abi_signature )]
|
||||
pub #data_type_definition
|
||||
})
|
||||
|
|
|
@ -229,6 +229,12 @@ pub struct InternalStructs {
|
|||
/// (function name) -> Vec<structs> all structs the function returns
|
||||
pub(crate) outputs: HashMap<String, Vec<String>>,
|
||||
|
||||
/// (event name, idx) -> struct which are the identifying properties we get the name
|
||||
/// from ethabi.
|
||||
///
|
||||
/// Note: we need to map the index of the event here because events can contain nameless inputs
|
||||
pub(crate) event_params: HashMap<(String, usize), String>,
|
||||
|
||||
/// All the structs extracted from the abi with their identifier as key
|
||||
pub(crate) structs: HashMap<String, SolStruct>,
|
||||
|
||||
|
@ -245,23 +251,37 @@ impl InternalStructs {
|
|||
let mut top_level_internal_types = HashMap::new();
|
||||
let mut function_params = HashMap::new();
|
||||
let mut outputs = HashMap::new();
|
||||
let mut event_params = HashMap::new();
|
||||
let mut structs = HashMap::new();
|
||||
for item in abi
|
||||
.into_iter()
|
||||
.filter(|item| item.type_field == "constructor" || item.type_field == "function")
|
||||
.filter(|item| matches!(item.type_field.as_str(), "constructor" | "function" | "event"))
|
||||
{
|
||||
let is_event = item.type_field == "event";
|
||||
|
||||
if let Some(name) = item.name {
|
||||
for input in item.inputs {
|
||||
for (idx, input) in item.inputs.into_iter().enumerate() {
|
||||
if let Some(ty) = input
|
||||
.internal_type
|
||||
.as_deref()
|
||||
.filter(|ty| ty.starts_with("struct "))
|
||||
.map(struct_type_identifier)
|
||||
{
|
||||
function_params.insert((name.clone(), input.name.clone()), ty.to_string());
|
||||
if is_event {
|
||||
event_params.insert((name.clone(), idx), ty.to_string());
|
||||
} else {
|
||||
function_params
|
||||
.insert((name.clone(), input.name.clone()), ty.to_string());
|
||||
}
|
||||
top_level_internal_types.insert(ty.to_string(), input);
|
||||
}
|
||||
}
|
||||
|
||||
if is_event {
|
||||
// no outputs in an event
|
||||
continue
|
||||
}
|
||||
|
||||
let mut output_structs = Vec::new();
|
||||
for output in item.outputs {
|
||||
if let Some(ty) = output
|
||||
|
@ -300,6 +320,7 @@ impl InternalStructs {
|
|||
function_params,
|
||||
outputs,
|
||||
structs,
|
||||
event_params,
|
||||
struct_tuples,
|
||||
rust_type_names: type_names
|
||||
.into_iter()
|
||||
|
@ -318,6 +339,15 @@ impl InternalStructs {
|
|||
.map(String::as_str)
|
||||
}
|
||||
|
||||
/// Returns the name of the rust type that will be generated if the given input is a struct
|
||||
/// This takes the index of event's parameter instead of the parameter's name like
|
||||
/// [`Self::get_function_input_struct_type`] does because we can't rely on the name since events
|
||||
/// support nameless parameters NOTE: this does not account for arrays or fixed arrays
|
||||
pub fn get_event_input_struct_type(&self, event: &str, idx: usize) -> Option<&str> {
|
||||
let key = (event.to_string(), idx);
|
||||
self.event_params.get(&key).and_then(|id| self.rust_type_names.get(id)).map(String::as_str)
|
||||
}
|
||||
|
||||
/// Returns the name of the rust type that will be generated if the given output is a struct
|
||||
/// NOTE: this does not account for arrays or fixed arrays
|
||||
pub fn get_function_output_struct_type(
|
||||
|
|
|
@ -828,12 +828,12 @@ mod tests {
|
|||
|
||||
let single_file = false;
|
||||
|
||||
multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
|
||||
multi_gen.clone().build().unwrap().write_to_module(mod_root, single_file).unwrap();
|
||||
multi_gen
|
||||
.clone()
|
||||
.build()
|
||||
.unwrap()
|
||||
.ensure_consistent_module(&mod_root, single_file)
|
||||
.ensure_consistent_module(mod_root, single_file)
|
||||
.expect("Inconsistent bindings");
|
||||
})
|
||||
}
|
||||
|
@ -845,12 +845,12 @@ mod tests {
|
|||
|
||||
let single_file = true;
|
||||
|
||||
multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
|
||||
multi_gen.clone().build().unwrap().write_to_module(mod_root, single_file).unwrap();
|
||||
multi_gen
|
||||
.clone()
|
||||
.build()
|
||||
.unwrap()
|
||||
.ensure_consistent_module(&mod_root, single_file)
|
||||
.ensure_consistent_module(mod_root, single_file)
|
||||
.expect("Inconsistent bindings");
|
||||
})
|
||||
}
|
||||
|
@ -868,13 +868,13 @@ mod tests {
|
|||
.clone()
|
||||
.build()
|
||||
.unwrap()
|
||||
.write_to_crate(name, version, &mod_root, single_file)
|
||||
.write_to_crate(name, version, mod_root, single_file)
|
||||
.unwrap();
|
||||
multi_gen
|
||||
.clone()
|
||||
.build()
|
||||
.unwrap()
|
||||
.ensure_consistent_crate(name, version, &mod_root, single_file, true)
|
||||
.ensure_consistent_crate(name, version, mod_root, single_file, true)
|
||||
.expect("Inconsistent bindings");
|
||||
})
|
||||
}
|
||||
|
@ -892,13 +892,13 @@ mod tests {
|
|||
.clone()
|
||||
.build()
|
||||
.unwrap()
|
||||
.write_to_crate(name, version, &mod_root, single_file)
|
||||
.write_to_crate(name, version, mod_root, single_file)
|
||||
.unwrap();
|
||||
multi_gen
|
||||
.clone()
|
||||
.build()
|
||||
.unwrap()
|
||||
.ensure_consistent_crate(name, version, &mod_root, single_file, true)
|
||||
.ensure_consistent_crate(name, version, mod_root, single_file, true)
|
||||
.expect("Inconsistent bindings");
|
||||
})
|
||||
}
|
||||
|
@ -910,7 +910,7 @@ mod tests {
|
|||
|
||||
let single_file = false;
|
||||
|
||||
multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
|
||||
multi_gen.clone().build().unwrap().write_to_module(mod_root, single_file).unwrap();
|
||||
|
||||
let mut cloned = multi_gen.clone();
|
||||
cloned.push(
|
||||
|
@ -924,7 +924,7 @@ mod tests {
|
|||
);
|
||||
|
||||
let result =
|
||||
cloned.build().unwrap().ensure_consistent_module(&mod_root, single_file).is_err();
|
||||
cloned.build().unwrap().ensure_consistent_module(mod_root, single_file).is_err();
|
||||
|
||||
// ensure inconsistent bindings are detected
|
||||
assert!(result, "Inconsistent bindings wrongly approved");
|
||||
|
@ -938,7 +938,7 @@ mod tests {
|
|||
|
||||
let single_file = true;
|
||||
|
||||
multi_gen.clone().build().unwrap().write_to_module(&mod_root, single_file).unwrap();
|
||||
multi_gen.clone().build().unwrap().write_to_module(mod_root, single_file).unwrap();
|
||||
|
||||
let mut cloned = multi_gen.clone();
|
||||
cloned.push(
|
||||
|
@ -952,7 +952,7 @@ mod tests {
|
|||
);
|
||||
|
||||
let result =
|
||||
cloned.build().unwrap().ensure_consistent_module(&mod_root, single_file).is_err();
|
||||
cloned.build().unwrap().ensure_consistent_module(mod_root, single_file).is_err();
|
||||
|
||||
// ensure inconsistent bindings are detected
|
||||
assert!(result, "Inconsistent bindings wrongly approved");
|
||||
|
@ -972,7 +972,7 @@ mod tests {
|
|||
.clone()
|
||||
.build()
|
||||
.unwrap()
|
||||
.write_to_crate(name, version, &mod_root, single_file)
|
||||
.write_to_crate(name, version, mod_root, single_file)
|
||||
.unwrap();
|
||||
|
||||
let mut cloned = multi_gen.clone();
|
||||
|
@ -989,7 +989,7 @@ mod tests {
|
|||
let result = cloned
|
||||
.build()
|
||||
.unwrap()
|
||||
.ensure_consistent_crate(name, version, &mod_root, single_file, true)
|
||||
.ensure_consistent_crate(name, version, mod_root, single_file, true)
|
||||
.is_err();
|
||||
|
||||
// ensure inconsistent bindings are detected
|
||||
|
@ -1010,7 +1010,7 @@ mod tests {
|
|||
.clone()
|
||||
.build()
|
||||
.unwrap()
|
||||
.write_to_crate(name, version, &mod_root, single_file)
|
||||
.write_to_crate(name, version, mod_root, single_file)
|
||||
.unwrap();
|
||||
|
||||
let mut cloned = multi_gen.clone();
|
||||
|
@ -1027,7 +1027,7 @@ mod tests {
|
|||
let result = cloned
|
||||
.build()
|
||||
.unwrap()
|
||||
.ensure_consistent_crate(name, version, &mod_root, single_file, true)
|
||||
.ensure_consistent_crate(name, version, mod_root, single_file, true)
|
||||
.is_err();
|
||||
|
||||
// ensure inconsistent bindings are detected
|
||||
|
|
|
@ -19,6 +19,7 @@ use std::{
|
|||
fn assert_codec<T: AbiDecode + AbiEncode>() {}
|
||||
fn assert_tokenizeable<T: Tokenizable>() {}
|
||||
fn assert_call<T: AbiEncode + AbiDecode + Default + Tokenizable>() {}
|
||||
fn assert_event<T: EthEvent>() {}
|
||||
|
||||
#[test]
|
||||
fn can_gen_human_readable() {
|
||||
|
@ -720,3 +721,18 @@ fn can_gen_large_tuple_array() {
|
|||
let _call = CallWithLongArrayCall::default();
|
||||
assert_call::<CallWithLongArrayCall>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_generate_event_with_structs() {
|
||||
/*
|
||||
contract MyContract {
|
||||
struct MyStruct {uint256 a; uint256 b; }
|
||||
event MyEvent(MyStruct, uint256);
|
||||
}
|
||||
*/
|
||||
abigen!(MyContract, "ethers-contract/tests/solidity-contracts/EventWithStruct.json");
|
||||
|
||||
let _filter = MyEventFilter { p0: MyStruct::default(), c: U256::zero() };
|
||||
assert_eq!("MyEvent((uint256,uint256),uint256)", MyEventFilter::abi_signature());
|
||||
assert_event::<MyEventFilter>();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
[
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "a",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "b",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"indexed": false,
|
||||
"internalType": "struct MyContract.MyStruct",
|
||||
"name": "",
|
||||
"type": "tuple"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "c",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "MyEvent",
|
||||
"type": "event"
|
||||
}
|
||||
]
|
|
@ -18,6 +18,11 @@ pub struct AbiParser {
|
|||
/// (function name, param name) -> struct which are the identifying properties we get the name
|
||||
/// from ethabi.
|
||||
pub function_params: HashMap<(String, String), String>,
|
||||
/// (event name, idx) -> struct which are the identifying properties we get the name
|
||||
/// from ethabi.
|
||||
///
|
||||
/// Note: we need to map the index of the event here because events can contain nameless inputs
|
||||
pub event_params: HashMap<(String, usize), String>,
|
||||
/// (function name) -> Vec<structs> all structs the function returns
|
||||
pub outputs: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
@ -170,12 +175,13 @@ impl AbiParser {
|
|||
structs: structs.into_iter().map(|s| (s.name().to_string(), s)).collect(),
|
||||
struct_tuples: HashMap::new(),
|
||||
function_params: Default::default(),
|
||||
event_params: Default::default(),
|
||||
outputs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a solidity event declaration from `event <name> (args*) anonymous?`
|
||||
pub fn parse_event(&self, s: &str) -> Result<Event> {
|
||||
pub fn parse_event(&mut self, s: &str) -> Result<Event> {
|
||||
let mut event = s.trim();
|
||||
if !event.starts_with("event ") {
|
||||
bail!("Not an event `{}`", s)
|
||||
|
@ -208,8 +214,20 @@ impl AbiParser {
|
|||
.split(',')
|
||||
.map(|e| self.parse_event_arg(e))
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(idx, (input, struct_name))| {
|
||||
if let Some(struct_name) = struct_name {
|
||||
// keep track of the user defined struct of that param
|
||||
self.event_params.insert((name.clone(), idx), struct_name);
|
||||
}
|
||||
input
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
return Ok(Event { name, inputs, anonymous })
|
||||
|
||||
let event = Event { name, inputs, anonymous };
|
||||
return Ok(event)
|
||||
}
|
||||
Some(' ') | Some('\t') => continue,
|
||||
Some(c) => {
|
||||
|
@ -220,7 +238,9 @@ impl AbiParser {
|
|||
}
|
||||
|
||||
/// Parse a single event param
|
||||
fn parse_event_arg(&self, input: &str) -> Result<EventParam> {
|
||||
///
|
||||
/// See [`Self::parse_type`]
|
||||
fn parse_event_arg(&self, input: &str) -> Result<(EventParam, Option<String>)> {
|
||||
let mut iter = input.trim().rsplitn(3, is_whitespace);
|
||||
let mut indexed = false;
|
||||
let mut name =
|
||||
|
@ -246,7 +266,8 @@ impl AbiParser {
|
|||
name = "";
|
||||
}
|
||||
|
||||
Ok(EventParam { name: name.to_string(), indexed, kind: self.parse_type(type_str)?.0 })
|
||||
let (kind, user_ty) = self.parse_type(type_str)?;
|
||||
Ok((EventParam { name: name.to_string(), indexed, kind }, user_ty))
|
||||
}
|
||||
|
||||
/// Returns the parsed function from the input string
|
||||
|
@ -665,12 +686,12 @@ mod tests {
|
|||
#[test]
|
||||
fn parse_event_input() {
|
||||
assert_eq!(
|
||||
AbiParser::default().parse_event_arg("address indexed x").unwrap(),
|
||||
AbiParser::default().parse_event_arg("address indexed x").unwrap().0,
|
||||
EventParam { name: "x".to_string(), kind: ParamType::Address, indexed: true }
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
AbiParser::default().parse_event_arg("address x").unwrap(),
|
||||
AbiParser::default().parse_event_arg("address x").unwrap().0,
|
||||
EventParam { name: "x".to_string(), kind: ParamType::Address, indexed: false }
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue