From 4684980acd55434dbdbec7551caf52cc2e625159 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 13 Mar 2021 11:51:50 +0100 Subject: [PATCH 01/12] refactor: extract error module and use error macros --- ethers-core/src/abi/error.rs | 25 ++++++++ ethers-core/src/abi/human_readable.rs | 85 +++++++++++---------------- ethers-core/src/abi/mod.rs | 8 ++- 3 files changed, 67 insertions(+), 51 deletions(-) create mode 100644 ethers-core/src/abi/error.rs diff --git a/ethers-core/src/abi/error.rs b/ethers-core/src/abi/error.rs new file mode 100644 index 000000000..2cb4a6682 --- /dev/null +++ b/ethers-core/src/abi/error.rs @@ -0,0 +1,25 @@ +//! Boilerplate error definitions. +use thiserror::Error; + +/// A type alias for std's Result with the Error as our error type. +pub type Result = std::result::Result; + +#[derive(Error, Debug)] +pub enum ParseError { + #[error("{0}")] + Message(String), + #[error(transparent)] + ParseError(#[from] super::Error), +} + +macro_rules! _format_err { + ($($tt:tt)*) => { + $crate::abi::ParseError::Message(format!($($tt)*)) + }; +} +pub(crate) use _format_err as format_err; + +macro_rules! _bail { + ($($tt:tt)*) => { return Err($crate::abi::error::format_err!($($tt)*)) }; +} +pub(crate) use _bail as bail; diff --git a/ethers-core/src/abi/human_readable.rs b/ethers-core/src/abi/human_readable.rs index 990152481..5e889740b 100644 --- a/ethers-core/src/abi/human_readable.rs +++ b/ethers-core/src/abi/human_readable.rs @@ -1,8 +1,7 @@ use std::collections::HashMap; -use thiserror::Error; - -use super::{ +use crate::abi::error::{bail, format_err, ParseError, Result}; +use crate::abi::{ param_type::Reader, Abi, Constructor, Event, EventParam, Function, Param, StateMutability, }; @@ -15,7 +14,7 @@ use super::{ /// "function x() external view returns (uint256)", /// ]).unwrap(); /// ``` -pub fn parse(input: &[&str]) -> Result { +pub fn parse(input: &[&str]) -> Result { let mut abi = Abi { constructor: None, functions: HashMap::new(), @@ -41,7 +40,7 @@ pub fn parse(input: &[&str]) -> Result { } else if line.starts_with("constructor") { abi.constructor = Some(parse_constructor(line)?); } else { - return Err(ParseError::ParseError(super::Error::InvalidData)); + bail!("Illegal abi `{}`", line) } } @@ -49,12 +48,12 @@ pub fn parse(input: &[&str]) -> Result { } /// Parses an identifier like event or function name -fn parse_identifier(input: &mut &str) -> Result { +pub(crate) fn parse_identifier(input: &mut &str) -> Result { let mut chars = input.trim_start().chars(); let mut name = String::new(); let c = chars .next() - .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + .ok_or_else(|| format_err!("Empty identifier in `{}`", input))?; if is_first_ident_char(c) { name.push(c); loop { @@ -67,30 +66,30 @@ fn parse_identifier(input: &mut &str) -> Result { } } } + if name.is_empty() { + return Err(ParseError::ParseError(super::Error::InvalidName( + input.to_owned(), + ))); + } *input = chars.as_str(); Ok(name) } /// Parses a solidity event declaration from `event (args*) anonymous?` -fn parse_event(mut event: &str) -> Result { - event = event.trim(); +fn parse_event(s: &str) -> Result { + let mut event = s.trim(); if !event.starts_with("event ") { - return Err(ParseError::ParseError(super::Error::InvalidData)); + bail!("Not an event `{}`", s) } event = &event[5..]; let name = parse_identifier(&mut event)?; - if name.is_empty() { - return Err(ParseError::ParseError(super::Error::InvalidName( - event.to_owned(), - ))); - } let mut chars = event.chars(); loop { match chars.next() { - None => return Err(ParseError::ParseError(super::Error::InvalidData)), + None => bail!("Expected event"), Some('(') => { event = chars.as_str().trim(); let mut anonymous = false; @@ -101,7 +100,7 @@ fn parse_event(mut event: &str) -> Result { event = event .trim() .strip_suffix(')') - .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + .ok_or_else(|| format_err!("Expected closing `)` in `{}`", s))?; let inputs = if event.is_empty() { Vec::new() @@ -120,26 +119,26 @@ fn parse_event(mut event: &str) -> Result { Some(' ') | Some('\t') => { continue; } - _ => { - return Err(ParseError::ParseError(super::Error::InvalidData)); + Some(c) => { + bail!("Illegal char `{}` at `{}`", c, s) } } } } /// Parse a single event param -fn parse_event_arg(input: &str) -> Result { +fn parse_event_arg(input: &str) -> Result { let mut iter = input.trim().rsplitn(3, is_whitespace); let mut indexed = false; let mut name = iter .next() - .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + .ok_or_else(|| format_err!("Empty event param at `{}`", input))?; if let Some(mid) = iter.next() { let kind; if let Some(ty) = iter.next() { if mid != "indexed" { - return Err(ParseError::ParseError(super::Error::InvalidData)); + bail!("Expected indexed keyword at `{}`", input) } indexed = true; kind = Reader::read(ty)?; @@ -164,24 +163,19 @@ fn parse_event_arg(input: &str) -> Result { } } -fn parse_function(mut input: &str) -> Result { - input = input.trim(); +fn parse_function(s: &str) -> Result { + let mut input = s.trim(); if !input.starts_with("function ") { - return Err(ParseError::ParseError(super::Error::InvalidData)); + bail!("Not a function `{}`", input) } input = &input[8..]; let name = parse_identifier(&mut input)?; - if name.is_empty() { - return Err(ParseError::ParseError(super::Error::InvalidName( - input.to_owned(), - ))); - } let mut iter = input.split(" returns"); let parens = iter .next() - .ok_or(ParseError::ParseError(super::Error::InvalidData))? + .ok_or_else(|| format_err!("Invalid function declaration at `{}`", s))? .trim_end(); let mut parens_iter = parens.rsplitn(2, ')'); @@ -209,7 +203,7 @@ fn parse_function(mut input: &str) -> Result { .trim() .strip_prefix('(') .and_then(|s| s.strip_suffix(')')) - .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + .ok_or_else(|| format_err!("Expected parentheses at `{}`", s))?; params .split(',') .filter(|s| !s.is_empty()) @@ -231,20 +225,20 @@ fn parse_function(mut input: &str) -> Result { }) } -fn parse_constructor(mut input: &str) -> Result { - input = input.trim(); +fn parse_constructor(s: &str) -> Result { + let mut input = s.trim(); if !input.starts_with("constructor") { - return Err(ParseError::ParseError(super::Error::InvalidData)); + bail!("Not a constructor `{}`", input) } input = input[11..] .trim_start() .strip_prefix('(') - .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + .ok_or_else(|| format_err!("Expected leading `(` in `{}`", s))?; let params = input .rsplitn(2, ')') .last() - .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + .ok_or_else(|| format_err!("Expected closing `)` in `{}`", s))?; let inputs = params .split(',') @@ -267,7 +261,7 @@ fn detect_state_mutability(s: &str) -> StateMutability { } } -fn parse_param(param: &str) -> Result { +fn parse_param(param: &str) -> Result { let mut iter = param.trim().rsplitn(3, is_whitespace); let name = iter @@ -294,15 +288,15 @@ fn parse_param(param: &str) -> Result { } } -fn is_first_ident_char(c: char) -> bool { +pub(crate) fn is_first_ident_char(c: char) -> bool { matches!(c, 'a'..='z' | 'A'..='Z' | '_') } -fn is_ident_char(c: char) -> bool { +pub(crate) fn is_ident_char(c: char) -> bool { matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_') } -fn is_whitespace(c: char) -> bool { +pub(crate) fn is_whitespace(c: char) -> bool { matches!(c, ' ' | '\t') } @@ -310,15 +304,6 @@ fn escape_quotes(input: &str) -> &str { input.trim_matches(is_whitespace).trim_matches('\"') } -#[derive(Error, Debug)] -pub enum ParseError { - #[error("expected data type")] - Kind, - - #[error(transparent)] - ParseError(#[from] super::Error), -} - #[cfg(test)] mod tests { use super::*; diff --git a/ethers-core/src/abi/mod.rs b/ethers-core/src/abi/mod.rs index 2a1317b77..3bf358e58 100644 --- a/ethers-core/src/abi/mod.rs +++ b/ethers-core/src/abi/mod.rs @@ -8,8 +8,14 @@ pub use ethabi::*; mod tokens; pub use tokens::{Detokenize, InvalidOutputType, Tokenizable, TokenizableItem, Tokenize}; +pub mod struct_def; +pub use struct_def::SolStruct; + +mod error; +pub use error::ParseError; + mod human_readable; -pub use human_readable::{parse as parse_abi, ParseError}; +pub use human_readable::parse as parse_abi; /// Extension trait for `ethabi::Function`. pub trait FunctionExt { From 4a29f3b6ae4d30579d42d66c097daf50cf90cd43 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 13 Mar 2021 11:52:14 +0100 Subject: [PATCH 02/12] feat: add solidity struct parser --- ethers-core/src/abi/struct_def.rs | 472 ++++++++++++++++++++++++++++++ 1 file changed, 472 insertions(+) create mode 100644 ethers-core/src/abi/struct_def.rs diff --git a/ethers-core/src/abi/struct_def.rs b/ethers-core/src/abi/struct_def.rs new file mode 100644 index 000000000..4422b0a76 --- /dev/null +++ b/ethers-core/src/abi/struct_def.rs @@ -0,0 +1,472 @@ +//! Solidity struct definition parsing support +use crate::abi::error::{bail, format_err, Result}; +use crate::abi::human_readable::{is_whitespace, parse_identifier}; +use crate::abi::{param_type::Reader, Param, ParamType}; + +/// A field declaration inside a struct +#[derive(Debug, Clone, PartialEq)] +pub struct FieldDeclaration { + name: String, + ty: FieldType, +} + +impl FieldDeclaration { + pub fn name(&self) -> &str { + &self.name + } +} + +/// A field declaration inside a struct +#[derive(Debug, Clone, PartialEq)] +pub enum FieldType { + /// Represents elementary types, see [`ParamType`] + Elementary(ParamType), + /// A non elementary type field, treated as user defined struct + Struct(StructFieldType), + // Array of user defined type + StructArray(StructFieldType), + // Array with fixed size of user defined type + FixedStructArray(StructFieldType, usize), + /// Mapping + Mapping(Box), +} + +impl FieldType { + pub fn is_mapping(&self) -> bool { + matches!(self, FieldType::Mapping(_)) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct MappingType { + /// key types can be elementary and `bytes` and `string` + /// + /// Valid `ParamType` variants are: + /// `Address`, `Bytes`, `Int`, `UInt`, `Bool`, `String`, `FixedBytes`, + key_type: ParamType, + /// The value type of this mapping + value_type: FieldType, +} + +/// Represents a elementary field declaration inside a struct with a : `int x` +#[derive(Debug, Clone, PartialEq)] +pub struct StructFieldDeclaration { + /// The name of the field + name: String, + /// The type of the field + ty: StructFieldType, +} + +/// How the type of a struct field is referenced +#[derive(Debug, Clone, PartialEq)] +pub struct StructFieldType { + /// The name of the struct + name: String, + /// All previous projections up until the name + /// + /// For `MostOuter.Outer.` this is `vec!["MostOuter", "Outer"]` + projections: Vec, +} + +/// Represents a solidity struct +#[derive(Debug, Clone, PartialEq)] +pub struct SolStruct { + name: String, + fields: Vec, +} + +impl SolStruct { + /// Parse a solidity struct definition + /// + /// # Example + /// + /// ``` + /// # use ethers::abi::SolStruct; + /// let s = SolStruct::parse("struct MyStruct { uint: x; uint: y;}").unwrap(); + /// ``` + pub fn parse(s: &str) -> Result { + let mut input = s.trim(); + if !input.starts_with("struct ") { + bail!("Not a struct `{}`", input) + } + input = &input[6..]; + + let name = parse_identifier(&mut input)?; + + let mut chars = input.chars(); + + loop { + match chars.next() { + None => bail!("Expected struct"), + Some('{') => { + // strip opening and trailing curly bracket + input = chars + .as_str() + .trim() + .strip_suffix('}') + .ok_or_else(|| format_err!("Expected closing `}}` in `{}`", s))? + .trim_end(); + + let fields = if input.is_empty() { + Vec::new() + } else { + input + .split(';') + .filter(|s| !s.is_empty()) + .map(parse_struct_field) + .collect::, _>>()? + }; + return Ok(SolStruct { name, fields }); + } + Some(' ') | Some('\t') => { + continue; + } + Some(c) => { + bail!("Illegal char `{}` at `{}`", c, s) + } + } + } + } + + /// Name of this struct + pub fn name(&self) -> &str { + &self.name + } + + /// All the fields of this struct + pub fn fields(&self) -> &Vec { + &self.fields + } + + /// Turns this struct type into a tuple type (`Vec`) + /// + /// This returns `None` if the struct contains a mapping, since mappings can only be parameters of internal solidity functions. + pub fn as_param_tuple_signature(&self) -> Option> { + let mut tuple = Vec::with_capacity(self.fields.len()); + for field in &self.fields { + match &field.ty { + FieldType::Elementary(param) => tuple.push(param.clone()), + FieldType::Struct(s) => {} + FieldType::StructArray(_) => {} + FieldType::FixedStructArray(_, _) => {} + FieldType::Mapping(_) => { + // mappings are not allowed as params in public functions + return None; + } + } + } + + todo!() + } +} + +/// Strips the identifier of field declaration from the input and returns it +fn strip_field_identifier(input: &mut &str) -> Result { + let mut iter = input.trim_end().rsplitn(2, is_whitespace); + let name = iter + .next() + .ok_or_else(|| format_err!("Expected field identifier")) + .map(|mut s| parse_identifier(&mut s))??; + *input = iter + .next() + .ok_or_else(|| format_err!("Expected field type in `{}`", input))? + .trim_end(); + Ok(name) +} + +/// Parses a field definition such as ` ? ` +fn parse_struct_field(s: &str) -> Result { + let mut input = s.trim_start(); + + if !input.starts_with("mapping") { + // strip potential defaults + input = input + .split('=') + .next() + .ok_or_else(|| format_err!("Expected field definition `{}`", s))? + .trim_end(); + } + let name = strip_field_identifier(&mut input)?; + Ok(FieldDeclaration { + name, + ty: parse_field_type(input)?, + }) +} + +fn parse_field_type(s: &str) -> Result { + let mut input = s.trim_start(); + if input.starts_with("mapping") { + return Ok(FieldType::Mapping(Box::new(parse_mapping(input)?))); + } + if input.ends_with(" payable") { + // special case for `address payable` + input = input[..input.len() - 7].trim_end(); + } + if let Ok(ty) = Reader::read(input) { + Ok(FieldType::Elementary(ty)) + } else { + // parsing elementary datatype failed, try struct + input = input.trim_end(); + let mut projections = Vec::new(); + + loop { + let ty = parse_identifier(&mut input)?; + let mut chars = input.chars(); + match chars.next() { + None => { + return Ok(FieldType::Struct(StructFieldType { + name: ty, + projections, + })) + } + Some(' ') | Some('\t') | Some('[') => { + // array + let mut size = String::new(); + loop { + match chars.next() { + None => bail!("Expected Array `{}`", input), + Some(' ') | Some('\t') => { + if !size.is_empty() { + bail!( + "Illegal whitespace in array size after `{}` in `{}`", + size, + input + ) + } + } + Some(']') => { + let ty = StructFieldType { + name: ty, + projections, + }; + + return if size.is_empty() { + Ok(FieldType::StructArray(ty)) + } else { + let size = size.parse().map_err(|_| { + format_err!("Illegal array size `{}` at `{}`", size, input) + })?; + Ok(FieldType::FixedStructArray(ty, size)) + }; + } + Some(c) => { + if c.is_numeric() { + size.push(c); + } else { + bail!("Illegal char `{}` inner array `{}`", c, input) + } + } + } + } + } + Some('.') => { + input = chars.as_str(); + projections.push(ty); + } + Some(c) => { + bail!("Illegal char `{}` at `{}`", c, input) + } + } + } + } +} + +/// parse a mapping declaration +fn parse_mapping(s: &str) -> Result { + let mut input = s.trim(); + if !input.starts_with("mapping") { + bail!("Not a mapping `{}`", input) + } + input = &input[7..].trim_start(); + let mut iter = input + .trim_start_matches('(') + .trim_end_matches(')') + .splitn(2, "=>"); + let key_type = iter + .next() + .ok_or_else(|| format_err!("Expected mapping key type at `{}`", input)) + .map(str::trim) + .map(Reader::read)??; + + if let ParamType::Array(_) | ParamType::FixedArray(_, _) | ParamType::Tuple(_) = &key_type { + bail!( + "Expected elementary mapping key type at `{}` got {:?}", + input, + key_type + ) + } + + let value_type = iter + .next() + .ok_or_else(|| format_err!("Expected mapping value type at `{}`", input)) + .map(str::trim) + .map(parse_field_type)??; + + Ok(MappingType { + key_type, + value_type, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_parse_simple_struct() { + assert_eq!( + SolStruct::parse("struct MyStruct{uint256 x; uint256 y;}").unwrap(), + SolStruct { + name: "MyStruct".to_string(), + fields: vec![ + FieldDeclaration { + name: "x".to_string(), + ty: FieldType::Elementary(ParamType::Uint(256)), + }, + FieldDeclaration { + name: "y".to_string(), + ty: FieldType::Elementary(ParamType::Uint(256)), + }, + ], + } + ); + } + + #[test] + fn can_parse_struct() { + assert_eq!( + SolStruct::parse("struct MyStruct{uint256 x; uint256 y; bytes[] _b; string[10] s; mapping(address => uint256) m;}").unwrap(), + SolStruct { + name: "MyStruct".to_string(), + fields: vec![ + FieldDeclaration { + name: "x".to_string(), + ty: FieldType::Elementary(ParamType::Uint(256)), + }, + FieldDeclaration { + name: "y".to_string(), + ty: FieldType::Elementary(ParamType::Uint(256)), + }, + FieldDeclaration { + name: "_b".to_string(), + ty: FieldType::Elementary(ParamType::Array(Box::new(ParamType::Bytes))), + }, + FieldDeclaration { + name: "s".to_string(), + ty: FieldType::Elementary(ParamType::FixedArray(Box::new(ParamType::String), 10)), + }, + FieldDeclaration { + name: "m".to_string(), + ty: FieldType::Mapping(Box::new( + MappingType { + key_type: ParamType::Address, + value_type: FieldType::Elementary(ParamType::Uint(256)) + } + )), + }, + ], + } + ); + } + + #[test] + fn can_parse_struct_projections() { + assert_eq!( + SolStruct::parse("struct MyStruct{uint256 x; Some.Other.Inner _other;}").unwrap(), + SolStruct { + name: "MyStruct".to_string(), + fields: vec![ + FieldDeclaration { + name: "x".to_string(), + ty: FieldType::Elementary(ParamType::Uint(256)), + }, + FieldDeclaration { + name: "_other".to_string(), + ty: FieldType::Struct(StructFieldType { + name: "Inner".to_string(), + projections: vec!["Some".to_string(), "Other".to_string()] + }), + }, + ], + } + ); + } + + #[test] + fn can_parse_structs() { + [ + "struct Demo {bytes x; address payable d;}", + "struct Demo2 {bytes[10] x; mapping(bool=> bool) d; int256 value;}", + "struct Struct { Other.MyStruct s; bool voted; address delegate; uint vote; }", + ] + .iter() + .for_each(|s| { + SolStruct::parse(s).unwrap(); + }); + } + + #[test] + fn can_parse_mapping_type() { + assert_eq!( + parse_mapping("mapping(string=> string)").unwrap(), + MappingType { + key_type: ParamType::String, + value_type: FieldType::Elementary(ParamType::String) + } + ); + } + + #[test] + fn can_parse_nested_mappings() { + assert_eq!( + parse_mapping("mapping(string=> mapping(string=> string))").unwrap(), + MappingType { + key_type: ParamType::String, + value_type: FieldType::Mapping(Box::new(MappingType { + key_type: ParamType::String, + value_type: FieldType::Elementary(ParamType::String), + })), + } + ); + } + + #[test] + fn can_detect_illegal_mappings_key_type() { + [ + "mapping(string[]=> mapping(string=> string))", + "mapping(bytes[10] => bool)", + "mapping(uint256[10] => bool)", + "mapping(Item=> bool)", + "mapping(Item[]=> mapping(address => bool))", + ] + .iter() + .for_each(|s| { + assert!(parse_mapping(s).is_err()); + }); + } + + #[test] + fn can_parse_mappings() { + [ + "mapping(string=> mapping(string=> string))", + "mapping(string=> mapping(string=> mapping(string=> mapping(string=> string))))", + "mapping(bool=> bool)", + "mapping(bytes32 => bool)", + "mapping(bytes=> bool)", + "mapping(uint256=> mapping(address => bool))", + ] + .iter() + .for_each(|s| { + parse_mapping(s).unwrap(); + }); + } + + #[test] + fn can_strip_field_ident() { + let mut s = "uint256 _myvar, + "; + let name = strip_field_identifier(&mut s).unwrap(); + assert_eq!("_myvar", name); + assert_eq!("uint256", s); + } +} From ee71b1d213285a0e511702cd5a6c7e0c85b8b830 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 13 Mar 2021 14:30:33 +0100 Subject: [PATCH 03/12] refactor: add AbiParse and support struct parsing --- ethers-core/src/abi/human_readable.rs | 672 ++++++++++++++++---------- ethers-core/src/abi/mod.rs | 2 +- ethers-core/src/abi/struct_def.rs | 174 +++---- 3 files changed, 503 insertions(+), 345 deletions(-) diff --git a/ethers-core/src/abi/human_readable.rs b/ethers-core/src/abi/human_readable.rs index 5e889740b..d2db867f5 100644 --- a/ethers-core/src/abi/human_readable.rs +++ b/ethers-core/src/abi/human_readable.rs @@ -1,252 +1,423 @@ -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use crate::abi::error::{bail, format_err, ParseError, Result}; +use crate::abi::struct_def::{FieldType, StructFieldType}; use crate::abi::{ - param_type::Reader, Abi, Constructor, Event, EventParam, Function, Param, StateMutability, + param_type::Reader, Abi, Constructor, Event, EventParam, Function, Param, ParamType, SolStruct, + StateMutability, }; -/// Parses a "human readable abi" string vector -/// -/// ``` -/// use ethers::abi::parse_abi; -/// -/// let abi = parse_abi(&[ -/// "function x() external view returns (uint256)", -/// ]).unwrap(); -/// ``` -pub fn parse(input: &[&str]) -> Result { - let mut abi = Abi { - constructor: None, - functions: HashMap::new(), - events: HashMap::new(), - receive: false, - fallback: false, - }; - - for mut line in input.iter().map(|s| escape_quotes(s)) { - line = line.trim_start(); - if line.starts_with("function") { - let function = parse_function(&line)?; - abi.functions - .entry(function.name.clone()) - .or_default() - .push(function); - } else if line.starts_with("event") { - let event = parse_event(line)?; - abi.events - .entry(event.name.clone()) - .or_default() - .push(event); - } else if line.starts_with("constructor") { - abi.constructor = Some(parse_constructor(line)?); - } else { - bail!("Illegal abi `{}`", line) - } +/// A parser that turns a "human readable abi" into a `Abi` +pub struct AbiParser { + abi: Abi, + /// solidity structs + structs: HashMap, + /// solidity structs as tuples + struct_tuples: HashMap>, +} + +impl AbiParser { + /// Parses a "human readable abi" string + pub fn parse_str(self, s: &str) -> Result { + self.parse(&s.lines().collect::>()) } - Ok(abi) -} + /// Parses a "human readable abi" string vector + /// + /// # Example + /// ``` + /// use ethers::abi::AbiParser; + /// + /// let abi = AbiParser::default().parse(&[ + /// "function x() external view returns (uint256)", + /// ]).unwrap(); + /// ``` + pub fn parse(mut self, input: &[&str]) -> Result { + // parse struct first + let (structs, types): (Vec<_>, Vec<_>) = input + .iter() + .map(|s| escape_quotes(s)) + .partition(|s| s.starts_with("struct")); + + for sol in structs { + let s = SolStruct::parse(sol)?; + if self.structs.contains_key(s.name()) { + bail!("Duplicate struct declaration for struct `{}`", s.name()) + } + self.structs.insert(s.name().to_string(), s); + } + self.substitute_structs()?; + + for mut line in types { + line = line.trim_start(); + if line.starts_with("function") { + let function = self.parse_function(&line)?; + self.abi + .functions + .entry(function.name.clone()) + .or_default() + .push(function); + } else if line.starts_with("event") { + let event = self.parse_event(line)?; + self.abi + .events + .entry(event.name.clone()) + .or_default() + .push(event); + } else if line.starts_with("constructor") { + self.abi.constructor = Some(self.parse_constructor(line)?); + } else { + bail!("Illegal abi `{}`", line) + } + } + Ok(self.abi) + } -/// Parses an identifier like event or function name -pub(crate) fn parse_identifier(input: &mut &str) -> Result { - let mut chars = input.trim_start().chars(); - let mut name = String::new(); - let c = chars - .next() - .ok_or_else(|| format_err!("Empty identifier in `{}`", input))?; - if is_first_ident_char(c) { - name.push(c); - loop { - match chars.clone().next() { - Some(c) if is_ident_char(c) => { - chars.next(); - name.push(c); + /// Substitutes any other struct references within structs with tuples + fn substitute_structs(&mut self) -> Result<()> { + let mut unresolved = self.structs.keys().collect::>(); + let mut sequential_retries = 0; + while let Some(name) = unresolved.pop_front() { + let mut resolved = true; + let sol = &self.structs[name]; + let mut tuple = Vec::with_capacity(sol.fields().len()); + for field in sol.fields() { + match field.r#type() { + FieldType::Elementary(param) => tuple.push(param.clone()), + FieldType::Struct(ty) => { + if let Some(param) = self.struct_tuples.get(ty.name()).cloned() { + tuple.push(ParamType::Tuple(param)) + } else { + resolved = false; + break; + } + } + FieldType::StructArray(ty) => { + if let Some(param) = self.struct_tuples.get(ty.name()).cloned() { + tuple.push(ParamType::Array(Box::new(ParamType::Tuple(param)))) + } else { + resolved = false; + break; + } + } + FieldType::FixedStructArray(ty, size) => { + if let Some(param) = self.struct_tuples.get(ty.name()).cloned() { + tuple.push(ParamType::FixedArray( + Box::new(ParamType::Tuple(param)), + *size, + )) + } else { + resolved = false; + break; + } + } + FieldType::Mapping(_) => { + bail!( + "mappings are not allowed as params in public functions of struct `{}`", + sol.name() + ) + } } - _ => break, + } + if resolved { + sequential_retries = 0; + self.struct_tuples.insert(sol.name().to_string(), tuple); + } else { + sequential_retries += 1; + if sequential_retries > unresolved.len() { + bail!("No struct definition found for struct `{}`", name) + } + unresolved.push_back(name); } } + Ok(()) } - if name.is_empty() { - return Err(ParseError::ParseError(super::Error::InvalidName( - input.to_owned(), - ))); - } - *input = chars.as_str(); - Ok(name) -} -/// Parses a solidity event declaration from `event (args*) anonymous?` -fn parse_event(s: &str) -> Result { - let mut event = s.trim(); - if !event.starts_with("event ") { - bail!("Not an event `{}`", s) + /// Link additional structs for parsing + pub fn with_structs(structs: Vec) -> Self { + Self { + abi: Abi { + constructor: None, + functions: HashMap::new(), + events: HashMap::new(), + receive: false, + fallback: false, + }, + structs: structs + .into_iter() + .map(|s| (s.name().to_string(), s)) + .collect(), + struct_tuples: HashMap::new(), + } } - event = &event[5..]; - let name = parse_identifier(&mut event)?; + /// Parses a solidity event declaration from `event (args*) anonymous?` + fn parse_event(&self, s: &str) -> Result { + let mut event = s.trim(); + if !event.starts_with("event ") { + bail!("Not an event `{}`", s) + } + event = &event[5..]; + + let name = parse_identifier(&mut event)?; - let mut chars = event.chars(); + let mut chars = event.chars(); - loop { - match chars.next() { - None => bail!("Expected event"), - Some('(') => { - event = chars.as_str().trim(); - let mut anonymous = false; - if event.ends_with("anonymous") { - anonymous = true; - event = event[..event.len() - 9].trim_end(); + loop { + match chars.next() { + None => bail!("Expected event"), + Some('(') => { + event = chars.as_str().trim(); + let mut anonymous = false; + if event.ends_with("anonymous") { + anonymous = true; + event = event[..event.len() - 9].trim_end(); + } + event = event + .trim() + .strip_suffix(')') + .ok_or_else(|| format_err!("Expected closing `)` in `{}`", s))?; + + let inputs = if event.is_empty() { + Vec::new() + } else { + event + .split(',') + .map(|e| self.parse_event_arg(e)) + .collect::, _>>()? + }; + return Ok(Event { + name, + inputs, + anonymous, + }); + } + Some(' ') | Some('\t') => { + continue; + } + Some(c) => { + bail!("Illegal char `{}` at `{}`", c, s) } - event = event - .trim() - .strip_suffix(')') - .ok_or_else(|| format_err!("Expected closing `)` in `{}`", s))?; - - let inputs = if event.is_empty() { - Vec::new() - } else { - event - .split(',') - .map(parse_event_arg) - .collect::, _>>()? - }; - return Ok(Event { - name, - inputs, - anonymous, - }); - } - Some(' ') | Some('\t') => { - continue; - } - Some(c) => { - bail!("Illegal char `{}` at `{}`", c, s) } } } -} - -/// Parse a single event param -fn parse_event_arg(input: &str) -> Result { - let mut iter = input.trim().rsplitn(3, is_whitespace); - let mut indexed = false; - let mut name = iter - .next() - .ok_or_else(|| format_err!("Empty event param at `{}`", input))?; - if let Some(mid) = iter.next() { - let kind; - if let Some(ty) = iter.next() { - if mid != "indexed" { - bail!("Expected indexed keyword at `{}`", input) - } - indexed = true; - kind = Reader::read(ty)?; - } else { - if name == "indexed" { + /// Parse a single event param + fn parse_event_arg(&self, input: &str) -> Result { + let mut iter = input.trim().rsplitn(3, is_whitespace); + let mut indexed = false; + let mut name = iter + .next() + .ok_or_else(|| format_err!("Empty event param at `{}`", input))?; + + let type_str; + if let Some(mid) = iter.next() { + if let Some(ty) = iter.next() { + if mid != "indexed" { + bail!("Expected indexed keyword at `{}`", input) + } indexed = true; - name = ""; + type_str = ty; + } else { + if name == "indexed" { + indexed = true; + name = ""; + } + type_str = mid; } - kind = Reader::read(mid)?; + } else { + type_str = name; + name = ""; } + Ok(EventParam { - name: name.to_owned(), - kind, + name: name.to_string(), indexed, + kind: self.parse_type(type_str)?, }) - } else { - Ok(EventParam { - name: "".to_owned(), - indexed, - kind: Reader::read(name)?, + } + + fn parse_function(&mut self, s: &str) -> Result { + let mut input = s.trim(); + if !input.starts_with("function ") { + bail!("Not a function `{}`", input) + } + input = &input[8..]; + let name = parse_identifier(&mut input)?; + + let mut iter = input.split(" returns"); + + let parens = iter + .next() + .ok_or_else(|| format_err!("Invalid function declaration at `{}`", s))? + .trim_end(); + + let mut parens_iter = parens.rsplitn(2, ')'); + let mut modifiers = parens_iter.next(); + + let input_params = if let Some(args) = parens_iter.next() { + args + } else { + modifiers + .take() + .ok_or(ParseError::ParseError(super::Error::InvalidData))? + } + .trim_start() + .strip_prefix('(') + .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + + let inputs = self.parse_params(input_params)?; + + let outputs = if let Some(params) = iter.next() { + let params = params + .trim() + .strip_prefix('(') + .and_then(|s| s.strip_suffix(')')) + .ok_or_else(|| format_err!("Expected parentheses at `{}`", s))?; + self.parse_params(params)? + } else { + Vec::new() + }; + + let state_mutability = modifiers.map(detect_state_mutability).unwrap_or_default(); + + #[allow(deprecated)] + Ok(Function { + name, + inputs, + outputs, + state_mutability, + constant: false, }) } -} -fn parse_function(s: &str) -> Result { - let mut input = s.trim(); - if !input.starts_with("function ") { - bail!("Not a function `{}`", input) + fn parse_params(&self, s: &str) -> Result> { + s.split(',') + .filter(|s| !s.is_empty()) + .map(|s| self.parse_param(s)) + .collect::, _>>() } - input = &input[8..]; - let name = parse_identifier(&mut input)?; - let mut iter = input.split(" returns"); + fn parse_type(&self, type_str: &str) -> Result { + if let Ok(kind) = Reader::read(type_str) { + Ok(kind) + } 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 tuple = self + .struct_tuples + .get(struct_ty.name()) + .cloned() + .map(ParamType::Tuple) + .ok_or_else(|| format_err!("Unknown struct `{}`", struct_ty.name()))?; + + match field { + FieldType::Struct(_) => Ok(tuple), + FieldType::StructArray(_) => Ok(ParamType::Array(Box::new(tuple))), + FieldType::FixedStructArray(_, size) => { + Ok(ParamType::FixedArray(Box::new(tuple), size)) + } + _ => bail!("Expected struct type"), + } + } else { + bail!("Failed determine event type `{}`", type_str) + } + } + } - let parens = iter - .next() - .ok_or_else(|| format_err!("Invalid function declaration at `{}`", s))? - .trim_end(); + fn parse_constructor(&self, s: &str) -> Result { + let mut input = s.trim(); + if !input.starts_with("constructor") { + bail!("Not a constructor `{}`", input) + } + input = input[11..] + .trim_start() + .strip_prefix('(') + .ok_or_else(|| format_err!("Expected leading `(` in `{}`", s))?; - let mut parens_iter = parens.rsplitn(2, ')'); - let mut modifiers = parens_iter.next(); + let params = input + .rsplitn(2, ')') + .last() + .ok_or_else(|| format_err!("Expected closing `)` in `{}`", s))?; - let input_params = if let Some(args) = parens_iter.next() { - args - } else { - modifiers - .take() - .ok_or(ParseError::ParseError(super::Error::InvalidData))? + let inputs = self.parse_params(params)?; + + Ok(Constructor { inputs }) } - .trim_start() - .strip_prefix('(') - .ok_or(ParseError::ParseError(super::Error::InvalidData))?; - - let inputs = input_params - .split(',') - .filter(|s| !s.is_empty()) - .map(parse_param) - .collect::, _>>()?; - - let outputs = if let Some(params) = iter.next() { - let params = params - .trim() - .strip_prefix('(') - .and_then(|s| s.strip_suffix(')')) - .ok_or_else(|| format_err!("Expected parentheses at `{}`", s))?; - params - .split(',') - .filter(|s| !s.is_empty()) - .map(parse_param) - .collect::, _>>()? - } else { - Vec::new() - }; - - let state_mutability = modifiers.map(detect_state_mutability).unwrap_or_default(); - - #[allow(deprecated)] - Ok(Function { - name, - inputs, - outputs, - state_mutability, - constant: false, - }) -} -fn parse_constructor(s: &str) -> Result { - let mut input = s.trim(); - if !input.starts_with("constructor") { - bail!("Not a constructor `{}`", input) + fn parse_param(&self, param: &str) -> Result { + let mut iter = param.trim().rsplitn(3, is_whitespace); + + let mut name = iter + .next() + .ok_or(ParseError::ParseError(super::Error::InvalidData))?; + + let type_str; + if let Some(ty) = iter.last() { + if name == "memory" || name == "calldata" { + name = ""; + } + type_str = ty; + } else { + type_str = name; + name = ""; + } + + Ok(Param { + name: name.to_string(), + kind: self.parse_type(type_str)?, + }) } - input = input[11..] - .trim_start() - .strip_prefix('(') - .ok_or_else(|| format_err!("Expected leading `(` in `{}`", s))?; +} - let params = input - .rsplitn(2, ')') - .last() - .ok_or_else(|| format_err!("Expected closing `)` in `{}`", s))?; +impl Default for AbiParser { + fn default() -> Self { + Self::with_structs(Vec::new()) + } +} - let inputs = params - .split(',') - .filter(|s| !s.is_empty()) - .map(parse_param) - .collect::, _>>()?; +/// Parses a "human readable abi" string vector +/// +/// ``` +/// use ethers::abi::parse_abi; +/// +/// let abi = parse_abi(&[ +/// "function x() external view returns (uint256)", +/// ]).unwrap(); +/// ``` +pub fn parse(input: &[&str]) -> Result { + AbiParser::default().parse(input) +} - Ok(Constructor { inputs }) +/// Parses an identifier like event or function name +pub(crate) fn parse_identifier(input: &mut &str) -> Result { + let mut chars = input.trim_start().chars(); + let mut name = String::new(); + let c = chars + .next() + .ok_or_else(|| format_err!("Empty identifier in `{}`", input))?; + if is_first_ident_char(c) { + name.push(c); + loop { + match chars.clone().next() { + Some(c) if is_ident_char(c) => { + chars.next(); + name.push(c); + } + _ => break, + } + } + } + if name.is_empty() { + return Err(ParseError::ParseError(super::Error::InvalidName( + input.to_string(), + ))); + } + *input = chars.as_str(); + Ok(name) } fn detect_state_mutability(s: &str) -> StateMutability { @@ -261,33 +432,6 @@ fn detect_state_mutability(s: &str) -> StateMutability { } } -fn parse_param(param: &str) -> Result { - let mut iter = param.trim().rsplitn(3, is_whitespace); - - let name = iter - .next() - .ok_or(ParseError::ParseError(super::Error::InvalidData))?; - - if let Some(ty) = iter.last() { - if name == "memory" || name == "calldata" { - Ok(Param { - name: "".to_owned(), - kind: Reader::read(ty)?, - }) - } else { - Ok(Param { - name: name.to_owned(), - kind: Reader::read(ty)?, - }) - } - } else { - Ok(Param { - name: "".to_owned(), - kind: Reader::read(name)?, - }) - } -} - pub(crate) fn is_first_ident_char(c: char) -> bool { matches!(c, 'a'..='z' | 'A'..='Z' | '_') } @@ -312,7 +456,7 @@ mod tests { #[test] fn parses_approve() { let fn_str = "function approve(address _spender, uint256 value) external returns(bool)"; - let parsed = parse_function(fn_str).unwrap(); + let parsed = AbiParser::default().parse_function(fn_str).unwrap(); assert_eq!(parsed.name, "approve"); assert_eq!(parsed.inputs[0].name, "_spender"); assert_eq!(parsed.inputs[0].kind, ParamType::Address,); @@ -325,7 +469,7 @@ mod tests { #[test] fn parses_function_arguments_return() { let fn_str = "function foo(uint32[] memory x) external view returns (address)"; - let parsed = parse_function(fn_str).unwrap(); + let parsed = AbiParser::default().parse_function(fn_str).unwrap(); assert_eq!(parsed.name, "foo"); assert_eq!(parsed.inputs[0].name, "x"); assert_eq!( @@ -339,7 +483,7 @@ mod tests { #[test] fn parses_function_empty() { let fn_str = "function foo()"; - let parsed = parse_function(fn_str).unwrap(); + let parsed = AbiParser::default().parse_function(fn_str).unwrap(); assert_eq!(parsed.name, "foo"); assert!(parsed.inputs.is_empty()); assert!(parsed.outputs.is_empty()); @@ -348,44 +492,46 @@ mod tests { #[test] fn parses_function_payable() { let fn_str = "function foo() public payable"; - let parsed = parse_function(fn_str).unwrap(); + let parsed = AbiParser::default().parse_function(fn_str).unwrap(); assert_eq!(parsed.state_mutability, StateMutability::Payable); } #[test] fn parses_function_view() { let fn_str = "function foo() external view"; - let parsed = parse_function(fn_str).unwrap(); + let parsed = AbiParser::default().parse_function(fn_str).unwrap(); assert_eq!(parsed.state_mutability, StateMutability::View); } #[test] fn parses_function_pure() { let fn_str = "function foo() pure"; - let parsed = parse_function(fn_str).unwrap(); + let parsed = AbiParser::default().parse_function(fn_str).unwrap(); assert_eq!(parsed.state_mutability, StateMutability::Pure); } #[test] fn parses_event() { assert_eq!( - parse_event(&mut "event Foo (address indexed x, uint y, bytes32[] z)").unwrap(), + AbiParser::default() + .parse_event(&mut "event Foo (address indexed x, uint y, bytes32[] z)") + .unwrap(), Event { anonymous: false, - name: "Foo".to_owned(), + name: "Foo".to_string(), inputs: vec![ EventParam { - name: "x".to_owned(), + name: "x".to_string(), kind: ParamType::Address, indexed: true, }, EventParam { - name: "y".to_owned(), + name: "y".to_string(), kind: ParamType::Uint(256), indexed: false, }, EventParam { - name: "z".to_owned(), + name: "z".to_string(), kind: ParamType::Array(Box::new(ParamType::FixedBytes(32))), indexed: false, }, @@ -397,10 +543,12 @@ mod tests { #[test] fn parses_anonymous_event() { assert_eq!( - parse_event(&mut "event Foo() anonymous").unwrap(), + AbiParser::default() + .parse_event(&mut "event Foo() anonymous") + .unwrap(), Event { anonymous: true, - name: "Foo".to_owned(), + name: "Foo".to_string(), inputs: vec![], } ); @@ -409,12 +557,14 @@ mod tests { #[test] fn parses_unnamed_event() { assert_eq!( - parse_event(&mut "event Foo(address)").unwrap(), + AbiParser::default() + .parse_event(&mut "event Foo(address)") + .unwrap(), Event { anonymous: false, - name: "Foo".to_owned(), + name: "Foo".to_string(), inputs: vec![EventParam { - name: "".to_owned(), + name: "".to_string(), kind: ParamType::Address, indexed: false, }], @@ -425,12 +575,14 @@ mod tests { #[test] fn parses_unnamed_indexed_event() { assert_eq!( - parse_event(&mut "event Foo(address indexed)").unwrap(), + AbiParser::default() + .parse_event(&mut "event Foo(address indexed)") + .unwrap(), Event { anonymous: false, - name: "Foo".to_owned(), + name: "Foo".to_string(), inputs: vec![EventParam { - name: "".to_owned(), + name: "".to_string(), kind: ParamType::Address, indexed: true, }], @@ -441,18 +593,20 @@ mod tests { #[test] fn parse_event_input() { assert_eq!( - parse_event_arg("address indexed x").unwrap(), + AbiParser::default() + .parse_event_arg("address indexed x") + .unwrap(), EventParam { - name: "x".to_owned(), + name: "x".to_string(), kind: ParamType::Address, indexed: true, } ); assert_eq!( - parse_event_arg("address x").unwrap(), + AbiParser::default().parse_event_arg("address x").unwrap(), EventParam { - name: "x".to_owned(), + name: "x".to_string(), kind: ParamType::Address, indexed: false, } @@ -471,7 +625,7 @@ mod tests { ] .iter() .for_each(|x| { - parse_function(x).unwrap(); + AbiParser::default().parse_function(x).unwrap(); }); } @@ -487,7 +641,7 @@ mod tests { ] .iter() .for_each(|x| { - parse_param(x).unwrap(); + AbiParser::default().parse_param(x).unwrap(); }); } diff --git a/ethers-core/src/abi/mod.rs b/ethers-core/src/abi/mod.rs index 3bf358e58..b09c91bea 100644 --- a/ethers-core/src/abi/mod.rs +++ b/ethers-core/src/abi/mod.rs @@ -15,7 +15,7 @@ mod error; pub use error::ParseError; mod human_readable; -pub use human_readable::parse as parse_abi; +pub use human_readable::{parse as parse_abi, AbiParser}; /// Extension trait for `ethabi::Function`. pub trait FunctionExt { diff --git a/ethers-core/src/abi/struct_def.rs b/ethers-core/src/abi/struct_def.rs index 4422b0a76..0440130c2 100644 --- a/ethers-core/src/abi/struct_def.rs +++ b/ethers-core/src/abi/struct_def.rs @@ -1,7 +1,7 @@ //! Solidity struct definition parsing support use crate::abi::error::{bail, format_err, Result}; use crate::abi::human_readable::{is_whitespace, parse_identifier}; -use crate::abi::{param_type::Reader, Param, ParamType}; +use crate::abi::{param_type::Reader, ParamType}; /// A field declaration inside a struct #[derive(Debug, Clone, PartialEq)] @@ -14,6 +14,10 @@ impl FieldDeclaration { pub fn name(&self) -> &str { &self.name } + + pub fn r#type(&self) -> &FieldType { + &self.ty + } } /// A field declaration inside a struct @@ -35,6 +39,15 @@ impl FieldType { pub fn is_mapping(&self) -> bool { matches!(self, FieldType::Mapping(_)) } + + pub(crate) fn as_struct(&self) -> Option<&StructFieldType> { + match self { + FieldType::Struct(s) + | FieldType::StructArray(s) + | FieldType::FixedStructArray(s, _) => Some(s), + _ => None, + } + } } #[derive(Debug, Clone, PartialEq)] @@ -68,6 +81,79 @@ pub struct StructFieldType { projections: Vec, } +impl StructFieldType { + pub fn name(&self) -> &str { + &self.name + } + + /// Parse a struct field declaration + /// + /// The parsed field is either a `Struct`, `StructArray` or `FixedStructArray` + pub fn parse(mut input: &str) -> Result { + let mut projections = Vec::new(); + + loop { + let ty = parse_identifier(&mut input)?; + let mut chars = input.chars(); + match chars.next() { + None => { + return Ok(FieldType::Struct(StructFieldType { + name: ty, + projections, + })) + } + Some(' ') | Some('\t') | Some('[') => { + // array + let mut size = String::new(); + loop { + match chars.next() { + None => bail!("Expected Array `{}`", input), + Some(' ') | Some('\t') => { + if !size.is_empty() { + bail!( + "Illegal whitespace in array size after `{}` in `{}`", + size, + input + ) + } + } + Some(']') => { + let ty = StructFieldType { + name: ty, + projections, + }; + + return if size.is_empty() { + Ok(FieldType::StructArray(ty)) + } else { + let size = size.parse().map_err(|_| { + format_err!("Illegal array size `{}` at `{}`", size, input) + })?; + Ok(FieldType::FixedStructArray(ty, size)) + }; + } + Some(c) => { + if c.is_numeric() { + size.push(c); + } else { + bail!("Illegal char `{}` inner array `{}`", c, input) + } + } + } + } + } + Some('.') => { + input = chars.as_str(); + projections.push(ty); + } + Some(c) => { + bail!("Illegal char `{}` at `{}`", c, input) + } + } + } + } +} + /// Represents a solidity struct #[derive(Debug, Clone, PartialEq)] pub struct SolStruct { @@ -82,7 +168,7 @@ impl SolStruct { /// /// ``` /// # use ethers::abi::SolStruct; - /// let s = SolStruct::parse("struct MyStruct { uint: x; uint: y;}").unwrap(); + /// let s = SolStruct::parse("struct MyStruct { uint x; uint y;}").unwrap(); /// ``` pub fn parse(s: &str) -> Result { let mut input = s.trim(); @@ -137,27 +223,6 @@ impl SolStruct { pub fn fields(&self) -> &Vec { &self.fields } - - /// Turns this struct type into a tuple type (`Vec`) - /// - /// This returns `None` if the struct contains a mapping, since mappings can only be parameters of internal solidity functions. - pub fn as_param_tuple_signature(&self) -> Option> { - let mut tuple = Vec::with_capacity(self.fields.len()); - for field in &self.fields { - match &field.ty { - FieldType::Elementary(param) => tuple.push(param.clone()), - FieldType::Struct(s) => {} - FieldType::StructArray(_) => {} - FieldType::FixedStructArray(_, _) => {} - FieldType::Mapping(_) => { - // mappings are not allowed as params in public functions - return None; - } - } - } - - todo!() - } } /// Strips the identifier of field declaration from the input and returns it @@ -206,68 +271,7 @@ fn parse_field_type(s: &str) -> Result { Ok(FieldType::Elementary(ty)) } else { // parsing elementary datatype failed, try struct - input = input.trim_end(); - let mut projections = Vec::new(); - - loop { - let ty = parse_identifier(&mut input)?; - let mut chars = input.chars(); - match chars.next() { - None => { - return Ok(FieldType::Struct(StructFieldType { - name: ty, - projections, - })) - } - Some(' ') | Some('\t') | Some('[') => { - // array - let mut size = String::new(); - loop { - match chars.next() { - None => bail!("Expected Array `{}`", input), - Some(' ') | Some('\t') => { - if !size.is_empty() { - bail!( - "Illegal whitespace in array size after `{}` in `{}`", - size, - input - ) - } - } - Some(']') => { - let ty = StructFieldType { - name: ty, - projections, - }; - - return if size.is_empty() { - Ok(FieldType::StructArray(ty)) - } else { - let size = size.parse().map_err(|_| { - format_err!("Illegal array size `{}` at `{}`", size, input) - })?; - Ok(FieldType::FixedStructArray(ty, size)) - }; - } - Some(c) => { - if c.is_numeric() { - size.push(c); - } else { - bail!("Illegal char `{}` inner array `{}`", c, input) - } - } - } - } - } - Some('.') => { - input = chars.as_str(); - projections.push(ty); - } - Some(c) => { - bail!("Illegal char `{}` at `{}`", c, input) - } - } - } + StructFieldType::parse(input.trim_end()) } } From 5d7d827e72deb5aa29cbcc72a4960aeb65523f4e Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 13 Mar 2021 15:25:13 +0100 Subject: [PATCH 04/12] test: add more struct parsing tests --- ethers-core/src/abi/human_readable.rs | 106 +++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/ethers-core/src/abi/human_readable.rs b/ethers-core/src/abi/human_readable.rs index d2db867f5..98ec7142d 100644 --- a/ethers-core/src/abi/human_readable.rs +++ b/ethers-core/src/abi/human_readable.rs @@ -451,7 +451,6 @@ fn escape_quotes(input: &str) -> &str { #[cfg(test)] mod tests { use super::*; - use crate::abi::ParamType; #[test] fn parses_approve() { @@ -629,6 +628,20 @@ mod tests { }); } + #[test] + fn can_parse_structs_and_functions() { + let abi = &[ + "struct Demo {bytes x; address payable d;}", + "struct Voter { uint weight; bool voted; address delegate; uint vote; }", + "event FireEvent(Voter v, NestedVoter2 n)", + "function foo(uint256[] memory x) external view returns (address)", + "function call(Voter memory voter) returns (address, uint256)", + "struct NestedVoter { Voter voter; bool voted; address delegate; uint vote; }", + "struct NestedVoter2 { NestedVoter[] voter; Voter[10] votes; address delegate; uint vote; }", + ]; + parse(abi).unwrap(); + } + #[test] fn can_parse_params() { [ @@ -653,4 +666,95 @@ mod tests { ]) .unwrap(); } + + #[test] + fn can_substitute_structs() { + let abi = parse(&[ + "struct MyStruct {int y; address _addr;}", + "event FireEvent(MyStruct m, address indexed newOwner)", + ]) + .unwrap(); + assert_eq!( + abi.events["FireEvent"][0].inputs.clone(), + vec![ + EventParam { + name: "m".to_string(), + kind: ParamType::Tuple(vec![ParamType::Int(256), ParamType::Address]), + indexed: false + }, + EventParam { + name: "newOwner".to_string(), + kind: ParamType::Address, + indexed: true + }, + ] + ); + } + + #[test] + fn can_substitute_array_structs() { + let abi = parse(&[ + "struct MyStruct {int y; address _addr;}", + "event FireEvent(MyStruct[] m, MyStruct[10] m2)", + ]) + .unwrap(); + + assert_eq!( + abi.events["FireEvent"][0].inputs.clone(), + vec![ + EventParam { + name: "m".to_string(), + kind: ParamType::Array(Box::new(ParamType::Tuple(vec![ + ParamType::Int(256), + ParamType::Address + ]))), + indexed: false + }, + EventParam { + name: "m2".to_string(), + kind: ParamType::FixedArray( + Box::new(ParamType::Tuple(vec![ + ParamType::Int(256), + ParamType::Address + ])), + 10 + ), + indexed: false + }, + ] + ); + } + + #[test] + fn can_substitute_nested_array_structs() { + let abi = parse(&[ + "struct MyStruct {int y; address _addr;}", + "event FireEvent(MyStruct[] m, MyStructWrapper w)", + "struct MyStructWrapper {MyStruct y; int y; address _addr;}", + ]) + .unwrap(); + + assert_eq!( + abi.events["FireEvent"][0].inputs.clone(), + vec![ + EventParam { + name: "m".to_string(), + kind: ParamType::Array(Box::new(ParamType::Tuple(vec![ + ParamType::Int(256), + ParamType::Address + ]))), + indexed: false + }, + EventParam { + name: "w".to_string(), + kind: ParamType::Tuple(vec![ + ParamType::Tuple(vec![ParamType::Int(256), ParamType::Address]), + ParamType::Int(256), + ParamType::Address + ]), + indexed: false + }, + ] + ); + } } From 9733aa6185e25c098cc5ea726b11bbe5f3d46035 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sun, 14 Mar 2021 14:40:03 +0100 Subject: [PATCH 05/12] feat: add EthAbiType proc macro derive for deriving Tokenizable trait --- .../ethers-contract-derive/src/lib.rs | 165 +++++++++++++++++- ethers-contract/src/lib.rs | 2 +- 2 files changed, 161 insertions(+), 6 deletions(-) diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs index c5c2922bb..d945842e0 100644 --- a/ethers-contract/ethers-contract-derive/src/lib.rs +++ b/ethers-contract/ethers-contract-derive/src/lib.rs @@ -2,14 +2,17 @@ //! ethereum smart contract. #![deny(missing_docs, unsafe_code)] -mod spanned; -use spanned::Spanned; +use proc_macro::TokenStream; + +use quote::{quote, quote_spanned}; +use syn::spanned::Spanned as _; +use syn::{parse::Error, parse_macro_input, Data, DeriveInput, Fields}; -mod abigen; use abigen::{expand, ContractArgs}; +use spanned::Spanned; -use proc_macro::TokenStream; -use syn::{parse::Error, parse_macro_input}; +mod abigen; +mod spanned; /// Proc macro to generate type-safe bindings to a contract. This macro accepts /// an Ethereum contract ABI or a path. Note that this path is rooted in @@ -65,3 +68,155 @@ pub fn abigen(input: TokenStream) -> TokenStream { .unwrap_or_else(|e| Error::new(span, format!("{:?}", e)).to_compile_error()) .into() } + +/// +#[proc_macro_derive(EthEvent, attributes(rename))] +pub fn derive_abi_event(input: TokenStream) -> TokenStream { + "fn answer() -> u32 { 42 }".parse().unwrap() +} + +/// Derives the `Tokenizable` trait for the labeled type. +/// +/// This derive macro automatically adds a type bound `field: Tokenizable` for each +/// field type. +#[proc_macro_derive(EthAbiType)] +pub fn derive_abi_type(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let generic_params = input.generics.params.iter().map(|p| quote! { #p }); + let generic_params = quote! { #(#generic_params,)* }; + + let generic_args = input.generics.type_params().map(|p| { + let name = &p.ident; + quote_spanned! { p.ident.span() => #name } + }); + + let generic_args = quote! { #(#generic_args,)* }; + + let generic_predicates = match input.generics.where_clause { + Some(ref clause) => { + let predicates = clause.predicates.iter().map(|p| quote! { #p }); + quote! { #(#predicates,)* } + } + None => quote! {}, + }; + + let (tokenize_predicates, params_len, init_struct_impl, into_token_impl) = match input.data { + Data::Struct(ref data) => match data.fields { + Fields::Named(ref fields) => { + let tokenize_predicates = fields.named.iter().map(|f| { + let ty = &f.ty; + quote_spanned! { f.span() => #ty: ethers_core::abi::Tokenize } + }); + let tokenize_predicates = quote! { #(#tokenize_predicates,)* }; + + let assignments = fields.named.iter().map(|f| { + let name = f.ident.as_ref().expect("Named fields have names"); + quote_spanned! { f.span() => #name: ethers_core::abi::Tokenizable::from_token(iter.next().expect("tokens size is sufficient qed").into_token())? } + }); + let init_struct_impl = quote! { Self { #(#assignments,)* } }; + + let into_token = fields.named.iter().map(|f| { + let name = f.ident.as_ref().expect("Named fields have names"); + quote_spanned! { f.span() => self.#name.into_token() } + }); + let into_token_impl = quote! { #(#into_token,)* }; + + ( + tokenize_predicates, + fields.named.len(), + init_struct_impl, + into_token_impl, + ) + } + Fields::Unnamed(ref fields) => { + let tokenize_predicates = fields.unnamed.iter().map(|f| { + let ty = &f.ty; + quote_spanned! { f.span() => #ty: ethers_core::abi::Tokenize } + }); + let tokenize_predicates = quote! { #(#tokenize_predicates,)* }; + + let assignments = fields.unnamed.iter().map(|f| { + quote_spanned! { f.span() => ethers_core::abi::Tokenizable::from_token(iter.next().expect("tokens size is sufficient qed").into_token())? } + }); + let init_struct_impl = quote! { Self(#(#assignments,)* ) }; + + let into_token = fields.unnamed.iter().enumerate().map(|(i, f)| { + let idx = syn::Index::from(i); + quote_spanned! { f.span() => self.#idx.into_token() } + }); + let into_token_impl = quote! { #(#into_token,)* }; + + ( + tokenize_predicates, + fields.unnamed.len(), + init_struct_impl, + into_token_impl, + ) + } + Fields::Unit => { + return TokenStream::from( + Error::new( + input.span(), + "EthAbiType cannot be derived for empty structs and unit", + ) + .to_compile_error(), + ) + } + }, + Data::Enum(_) => { + return TokenStream::from( + Error::new(input.span(), "EthAbiType cannot be derived for enums") + .to_compile_error(), + ) + } + Data::Union(_) => { + return TokenStream::from( + Error::new(input.span(), "EthAbiType cannot be derived for unions") + .to_compile_error(), + ) + } + }; + + let tokenizeable_impl = quote! { + impl<#generic_params> ethers_core::abi::Tokenizable for #name<#generic_args> + where + #generic_predicates + #tokenize_predicates + { + + fn from_token(token: ethers_core::abi::Token) -> Result where + Self: Sized { + if let ethers_core::abi::Token::Tuple(tokens) = token { + if tokens.len() != #params_len { + return Err(ethers_core::abi::InvalidOutputType(format!( + "Expected {} tokens, got {}: {:?}", + #params_len, + tokens.len(), + tokens + ))); + } + + let mut iter = tokens.into_iter(); + + Ok(#init_struct_impl) + } else { + Err(ethers_core::abi::InvalidOutputType(format!( + "Expected Tuple, got {:?}", + token + ))) + } + } + + fn into_token(self) -> ethers_core::abi::Token { + ethers_core::abi::Token::Tuple( + vec![ + #into_token_impl + ] + ) + } + } + }; + + TokenStream::from(tokenizeable_impl) +} diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index fe2f0fa83..9af0d5502 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -46,7 +46,7 @@ pub use ethers_contract_abigen::Abigen; #[cfg(feature = "abigen")] #[cfg_attr(docsrs, doc(cfg(feature = "abigen")))] -pub use ethers_contract_derive::abigen; +pub use ethers_contract_derive::{abigen, EthAbiType, EthEvent}; // Hide the Lazy re-export, it's just for convenience #[doc(hidden)] From b5efdb0231992c6c1f10ca72a188ca3524234821 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sun, 14 Mar 2021 14:40:28 +0100 Subject: [PATCH 06/12] test: add EthAbiType derive tests --- .../ethers-contract-derive/src/lib.rs | 2 +- ethers-contract/tests/common/derive.rs | 81 +++++++++++++++++++ ethers-contract/tests/common/mod.rs | 1 + 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 ethers-contract/tests/common/derive.rs diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs index d945842e0..e5172bc20 100644 --- a/ethers-contract/ethers-contract-derive/src/lib.rs +++ b/ethers-contract/ethers-contract-derive/src/lib.rs @@ -82,7 +82,7 @@ pub fn derive_abi_event(input: TokenStream) -> TokenStream { #[proc_macro_derive(EthAbiType)] pub fn derive_abi_type(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - + let name = &input.ident; let generic_params = input.generics.params.iter().map(|p| quote! { #p }); let generic_params = quote! { #(#generic_params,)* }; diff --git a/ethers-contract/tests/common/derive.rs b/ethers-contract/tests/common/derive.rs new file mode 100644 index 000000000..ac0fcf14b --- /dev/null +++ b/ethers-contract/tests/common/derive.rs @@ -0,0 +1,81 @@ +use ethers_contract::EthAbiType; +use ethers_core::abi::Tokenizable; +use ethers_core::types::Address; + +#[derive(Debug, Clone, PartialEq, EthAbiType)] +pub struct ValueChanged { + pub old_author: Address, + pub new_author: Address, + pub old_value: String, + pub new_value: String, +} + +#[derive(Debug, Clone, PartialEq, EthAbiType)] +pub struct ValueChangedWrapper { + pub inner: ValueChanged, + pub msg: String, +} + +#[derive(Debug, Clone, PartialEq, EthAbiType)] +pub struct ValueChangedTuple(Address, Address, String, String); + +#[derive(Debug, Clone, PartialEq, EthAbiType)] +pub struct ValueChangedTupleWrapper(ValueChangedTuple, String); + +#[test] +fn can_detokenize_struct() { + let value = ValueChanged { + old_author: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap(), + new_author: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse().unwrap(), + old_value: "50".to_string(), + new_value: "100".to_string(), + }; + + let token = value.clone().into_token(); + assert_eq!(value, ValueChanged::from_token(token).unwrap()); +} + +#[test] +fn can_detokenize_nested_structs() { + let value = ValueChangedWrapper { + inner: ValueChanged { + old_author: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap(), + new_author: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse().unwrap(), + old_value: "50".to_string(), + new_value: "100".to_string(), + }, + msg: "hello world".to_string(), + }; + + let token = value.clone().into_token(); + assert_eq!(value, ValueChangedWrapper::from_token(token).unwrap()); +} + +#[test] +fn can_detokenize_tuple_struct() { + let value = ValueChangedTuple( + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap(), + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse().unwrap(), + "50".to_string(), + "100".to_string(), + ); + + let token = value.clone().into_token(); + assert_eq!(value, ValueChangedTuple::from_token(token).unwrap()); +} + +#[test] +fn can_detokenize_nested_tuple_struct() { + let value = ValueChangedTupleWrapper( + ValueChangedTuple( + "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap(), + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse().unwrap(), + "50".to_string(), + "100".to_string(), + ), + "hello world".to_string(), + ); + + let token = value.clone().into_token(); + assert_eq!(value, ValueChangedTupleWrapper::from_token(token).unwrap()); +} diff --git a/ethers-contract/tests/common/mod.rs b/ethers-contract/tests/common/mod.rs index 065a2482d..4ebcc9ad6 100644 --- a/ethers-contract/tests/common/mod.rs +++ b/ethers-contract/tests/common/mod.rs @@ -1,3 +1,4 @@ +mod derive; use ethers_core::{ abi::{Abi, Detokenize, InvalidOutputType, Token}, types::{Address, Bytes}, From 70aea914d1730b64a04290a08043d08acf0fe294 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sun, 14 Mar 2021 15:02:18 +0100 Subject: [PATCH 07/12] refactor: extract tokenizeable implementation in separate method --- ethers-contract/ethers-contract-derive/src/lib.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs index e5172bc20..8ede05760 100644 --- a/ethers-contract/ethers-contract-derive/src/lib.rs +++ b/ethers-contract/ethers-contract-derive/src/lib.rs @@ -69,10 +69,15 @@ pub fn abigen(input: TokenStream) -> TokenStream { .into() } +/// Derives the `EthEvent` trait for the labeled type. /// +/// Additional arguments can be specified using the `#[ethevent(...)]` attribute: +/// +/// - `rename`: Overrides the generated `EthEvent` name, default is the struct's name. #[proc_macro_derive(EthEvent, attributes(rename))] pub fn derive_abi_event(input: TokenStream) -> TokenStream { - "fn answer() -> u32 { 42 }".parse().unwrap() + let input = parse_macro_input!(input as DeriveInput); + todo!() } /// Derives the `Tokenizable` trait for the labeled type. @@ -82,6 +87,10 @@ pub fn derive_abi_event(input: TokenStream) -> TokenStream { #[proc_macro_derive(EthAbiType)] pub fn derive_abi_type(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); + derive_tokenizeable_impl(&input) +} + +fn derive_tokenizeable_impl(input: &DeriveInput) -> TokenStream { let name = &input.ident; let generic_params = input.generics.params.iter().map(|p| quote! { #p }); let generic_params = quote! { #(#generic_params,)* }; From 5c2829cb99e7cb2f40ef73f101743e339748e0c0 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sun, 14 Mar 2021 15:03:10 +0100 Subject: [PATCH 08/12] chore(test): use EthAbiType derive instead implementing Detokenizeable --- ethers-contract/tests/common/mod.rs | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/ethers-contract/tests/common/mod.rs b/ethers-contract/tests/common/mod.rs index 4ebcc9ad6..fe3f8773d 100644 --- a/ethers-contract/tests/common/mod.rs +++ b/ethers-contract/tests/common/mod.rs @@ -1,18 +1,19 @@ mod derive; use ethers_core::{ - abi::{Abi, Detokenize, InvalidOutputType, Token}, + abi::Abi, types::{Address, Bytes}, }; -use ethers_contract::{Contract, ContractFactory}; +use ethers_contract::{Contract, ContractFactory, EthAbiType}; use ethers_core::utils::{GanacheInstance, Solc}; use ethers_middleware::signer::SignerMiddleware; use ethers_providers::{Http, Middleware, Provider}; use ethers_signers::LocalWallet; use std::{convert::TryFrom, sync::Arc, time::Duration}; -// Note: We also provide the `abigen` macro for generating these bindings automatically -#[derive(Clone, Debug)] +// Note: The `EthAbiType` derive macro implements the necessary conversion between `Tokens` and +// the struct +#[derive(Clone, Debug, EthAbiType)] pub struct ValueChanged { pub old_author: Address, pub new_author: Address, @@ -20,22 +21,6 @@ pub struct ValueChanged { pub new_value: String, } -impl Detokenize for ValueChanged { - fn from_tokens(tokens: Vec) -> Result { - let old_author: Address = tokens[1].clone().into_address().unwrap(); - let new_author: Address = tokens[1].clone().into_address().unwrap(); - let old_value = tokens[2].clone().into_string().unwrap(); - let new_value = tokens[3].clone().into_string().unwrap(); - - Ok(Self { - old_author, - new_author, - old_value, - new_value, - }) - } -} - /// compiles the given contract and returns the ABI and Bytecode pub fn compile_contract(name: &str, filename: &str) -> (Abi, Bytes) { let compiled = Solc::new(&format!("./tests/solidity-contracts/{}", filename)) From a5cac02d7a195bf54b6ffd06a562d5130ffc8816 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sun, 14 Mar 2021 20:50:38 +0100 Subject: [PATCH 09/12] feat: introduce EthEvent trait --- ethers-contract/src/event.rs | 16 ++++++++++++++++ ethers-contract/src/lib.rs | 1 + 2 files changed, 17 insertions(+) diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs index 15a72116a..da659217d 100644 --- a/ethers-contract/src/event.rs +++ b/ethers-contract/src/event.rs @@ -6,6 +6,22 @@ use ethers_core::{ }; use ethers_providers::{FilterWatcher, Middleware, PubsubClient, SubscriptionStream}; use std::marker::PhantomData; +use std::borrow::Cow; + +/// A trait for implementing event bindings +pub trait EthEvent: Detokenize { + /// The name of the event this type represents + fn name(&self) -> Cow<'static, str>; + + /// Retrieves the signature for the event this data corresponds to. + /// This signature is the Keccak-256 hash of the ABI signature of + /// this event. + fn signature() -> H256; + + /// Retrieves the ABI signature for the event this data corresponds + /// to. + fn abi_signature() -> Cow<'static, str>; +} /// Helper for managing the event filter before querying or streaming its logs #[derive(Debug)] diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index 9af0d5502..84a8c87d3 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -26,6 +26,7 @@ mod factory; pub use factory::ContractFactory; mod event; +pub use event::EthEvent; mod stream; From f669141a5b264b9fc0df52e3f0ab49fc87a1029c Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sun, 14 Mar 2021 20:53:33 +0100 Subject: [PATCH 10/12] feat: add EthEvent proc macro derive support --- Cargo.lock | 3 + .../ethers-contract-derive/Cargo.toml | 2 +- .../ethers-contract-derive/src/lib.rs | 468 +++++++++++++++--- ethers-core/src/abi/human_readable.rs | 6 +- 4 files changed, 416 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d6d7771e..e6f13ecb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "Inflector" version = "0.11.4" @@ -672,6 +674,7 @@ version = "0.2.2" dependencies = [ "ethers-contract-abigen", "ethers-core", + "hex", "proc-macro2", "quote", "serde_json", diff --git a/ethers-contract/ethers-contract-derive/Cargo.toml b/ethers-contract/ethers-contract-derive/Cargo.toml index b0caecbed..1a6c14ac8 100644 --- a/ethers-contract/ethers-contract-derive/Cargo.toml +++ b/ethers-contract/ethers-contract-derive/Cargo.toml @@ -17,7 +17,7 @@ ethers-core = { version = "0.2.2", path = "../../ethers-core" } ethers-contract-abigen = { version = "0.2.2", path = "../ethers-contract-abigen" } serde_json = "1.0.53" - +hex = { version = "0.4.3", default-features = false, features = ["std"] } proc-macro2 = "1.0" quote = "1.0" syn = "1.0.12" diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs index 8ede05760..37aa94cb8 100644 --- a/ethers-contract/ethers-contract-derive/src/lib.rs +++ b/ethers-contract/ethers-contract-derive/src/lib.rs @@ -2,13 +2,19 @@ //! ethereum smart contract. #![deny(missing_docs, unsafe_code)] +use ethers_contract_abigen::Source; use proc_macro::TokenStream; - +use proc_macro2::{Literal, Span}; use quote::{quote, quote_spanned}; use syn::spanned::Spanned as _; -use syn::{parse::Error, parse_macro_input, Data, DeriveInput, Fields}; +use syn::{ + parse::Error, parse_macro_input, AttrStyle, Data, DeriveInput, Expr, Fields, GenericArgument, + Lit, Meta, NestedMeta, PathArguments, Type, +}; use abigen::{expand, ContractArgs}; +use ethers_core::abi::{AbiParser, Event, EventExt, EventParam, ParamType}; +use hex::FromHex; use spanned::Spanned; mod abigen; @@ -69,15 +75,237 @@ pub fn abigen(input: TokenStream) -> TokenStream { .into() } -/// Derives the `EthEvent` trait for the labeled type. +/// Derives the `EthEvent` and `Tokenizeable` trait for the labeled type. /// /// Additional arguments can be specified using the `#[ethevent(...)]` attribute: /// -/// - `rename`: Overrides the generated `EthEvent` name, default is the struct's name. -#[proc_macro_derive(EthEvent, attributes(rename))] +/// - `name`, `name = "..."`: Overrides the generated `EthEvent` name, default is the struct's name. +/// - `signature`, `signature = "..."`: The signature as hex string to override the event's signature. +/// - `abi`, `abi = "..."`: The ABI signature for the event this event's data corresponds to. +#[proc_macro_derive(EthEvent, attributes(ethevent))] pub fn derive_abi_event(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - todo!() + let name = &input.ident; + let attributes = match parse_attributes(&input) { + Ok(attributes) => attributes, + Err(errors) => return TokenStream::from(errors), + }; + + let event_name = attributes + .name + .map(|(n, _)| n) + .unwrap_or_else(|| input.ident.to_string()); + + let (abi, hash) = if let Some((src, span)) = attributes.abi { + if let Ok(mut event) = parse_event(&src) { + event.name = event_name.clone(); + (event.abi_signature(), event.signature()) + } else { + match src.parse::().and_then(|s| s.get()) { + Ok(abi) => { + // try to derive the signature from the abi from the parsed abi + // TODO(mattsse): this will fail for events that contain other non elementary types in their abi + // because the parser doesn't know how to substitute the types + // this could be mitigated by getting the ABI of each non elementary type at runtime + // and computing the the signature as `static Lazy::...` + match parse_event(&abi) { + Ok(mut event) => { + event.name = event_name.clone(); + (event.abi_signature(), event.signature()) + } + Err(err) => { + return TokenStream::from(Error::new(span, err).to_compile_error()) + } + } + } + Err(err) => return TokenStream::from(Error::new(span, err).to_compile_error()), + } + } + } else { + // try to determine the abi from the fields + match derive_abi_event_from_fields(&input) { + Ok(mut event) => { + event.name = event_name.clone(); + (event.abi_signature(), event.signature()) + } + Err(err) => return TokenStream::from(err.to_compile_error()), + } + }; + + let signature = if let Some((hash, _)) = attributes.signature_hash { + signature(&hash) + } else { + signature(hash.as_bytes()) + }; + + let ethevent_impl = quote! { + impl ethers_contract::EthEvent for #name { + + fn name(&self) -> ::std::borrow::Cow<'static, str> { + #event_name.into() + } + + fn signature() -> ethers_core::types::H256 { + #signature + } + + fn abi_signature() -> ::std::borrow::Cow<'static, str> { + #abi.into() + } + } + }; + + let tokenize_impl = derive_tokenizeable_impl(&input); + + // parse attributes abi into source + TokenStream::from(quote! { + #tokenize_impl + #ethevent_impl + }) +} + +fn derive_abi_event_from_fields(input: &DeriveInput) -> Result { + let types: Vec<_> = match input.data { + Data::Struct(ref data) => match data.fields { + Fields::Named(ref fields) => fields.named.iter().map(|f| &f.ty).collect(), + Fields::Unnamed(ref fields) => fields.unnamed.iter().map(|f| &f.ty).collect(), + Fields::Unit => { + return Err(Error::new( + input.span(), + "EthEvent cannot be derived for empty structs and unit", + )) + } + }, + Data::Enum(_) => { + return Err(Error::new( + input.span(), + "EthEvent cannot be derived for enums", + )); + } + Data::Union(_) => { + return Err(Error::new( + input.span(), + "EthEvent cannot be derived for unions", + )); + } + }; + + let inputs = types + .iter() + .map(|ty| find_parameter_type(ty)) + .collect::, _>>()?; + + let event = Event { + name: "".to_string(), + inputs: inputs + .into_iter() + .map(|kind| EventParam { + name: "".to_string(), + kind, + indexed: false, + }) + .collect(), + anonymous: false, + }; + Ok(event) +} + +fn find_parameter_type(ty: &Type) -> Result { + match ty { + Type::Array(ty) => { + let param = find_parameter_type(ty.elem.as_ref())?; + if let Expr::Lit(ref expr) = ty.len { + if let Lit::Int(ref len) = expr.lit { + if let Ok(size) = len.base10_parse::() { + return Ok(ParamType::FixedArray(Box::new(param), size)); + } + } + } + Err(Error::new( + ty.span(), + "Failed to derive proper ABI from array field", + )) + } + Type::Path(ty) => { + if let Some(ident) = ty.path.get_ident() { + return match ident.to_string().to_lowercase().as_str() { + "address" => Ok(ParamType::Address), + "string" => Ok(ParamType::String), + "bool" => Ok(ParamType::Bool), + "int" | "uint" => Ok(ParamType::Uint(256)), + "h160" => Ok(ParamType::FixedBytes(20)), + "h256" | "secret" | "hash" => Ok(ParamType::FixedBytes(32)), + "h512" | "public" => Ok(ParamType::FixedBytes(64)), + s => parse_int_param_type(s).ok_or_else(|| { + Error::new(ty.span(), "Failed to derive proper ABI from fields") + }), + }; + } + // check for `Vec` + if ty.path.segments.len() == 1 && ty.path.segments[0].ident == "Vec" { + if let PathArguments::AngleBracketed(ref args) = ty.path.segments[0].arguments { + if args.args.len() == 1 { + if let GenericArgument::Type(ref ty) = args.args.iter().next().unwrap() { + let kind = find_parameter_type(ty)?; + return Ok(ParamType::Array(Box::new(kind))); + } + } + } + } + + Err(Error::new( + ty.span(), + "Failed to derive proper ABI from fields", + )) + } + Type::Tuple(ty) => { + let params = ty + .elems + .iter() + .map(|t| find_parameter_type(t)) + .collect::, _>>()?; + Ok(ParamType::Tuple(params)) + } + _ => { + eprintln!("Found other types"); + Err(Error::new( + ty.span(), + "Failed to derive proper ABI from fields", + )) + } + } +} + +fn parse_int_param_type(s: &str) -> Option { + let size = s + .chars() + .skip(1) + .collect::() + .parse::() + .ok()?; + if s.starts_with('u') { + Some(ParamType::Uint(size)) + } else if s.starts_with('i') { + Some(ParamType::Int(size)) + } else { + None + } +} + +fn signature(hash: &[u8]) -> proc_macro2::TokenStream { + let bytes = hash.iter().copied().map(Literal::u8_unsuffixed); + quote! {ethers_core::types::H256([#( #bytes ),*])} +} + +fn parse_event(abi: &str) -> Result { + let abi = if !abi.trim_start().starts_with("event ") { + format!("event {}", abi) + } else { + abi.to_string() + }; + AbiParser::default() + .parse_event(&abi) + .map_err(|err| format!("Failed to parse the event ABI: {:?}", err)) } /// Derives the `Tokenizable` trait for the labeled type. @@ -87,10 +315,10 @@ pub fn derive_abi_event(input: TokenStream) -> TokenStream { #[proc_macro_derive(EthAbiType)] pub fn derive_abi_type(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - derive_tokenizeable_impl(&input) + TokenStream::from(derive_tokenizeable_impl(&input)) } -fn derive_tokenizeable_impl(input: &DeriveInput) -> TokenStream { +fn derive_tokenizeable_impl(input: &DeriveInput) -> proc_macro2::TokenStream { let name = &input.ident; let generic_params = input.generics.params.iter().map(|p| quote! { #p }); let generic_params = quote! { #(#generic_params,)* }; @@ -164,68 +392,190 @@ fn derive_tokenizeable_impl(input: &DeriveInput) -> TokenStream { ) } Fields::Unit => { - return TokenStream::from( - Error::new( - input.span(), - "EthAbiType cannot be derived for empty structs and unit", - ) - .to_compile_error(), + return Error::new( + input.span(), + "EthAbiType cannot be derived for empty structs and unit", ) + .to_compile_error(); } }, Data::Enum(_) => { - return TokenStream::from( - Error::new(input.span(), "EthAbiType cannot be derived for enums") - .to_compile_error(), - ) + return Error::new(input.span(), "EthAbiType cannot be derived for enums") + .to_compile_error(); } Data::Union(_) => { - return TokenStream::from( - Error::new(input.span(), "EthAbiType cannot be derived for unions") - .to_compile_error(), - ) + return Error::new(input.span(), "EthAbiType cannot be derived for unions") + .to_compile_error(); } }; - let tokenizeable_impl = quote! { - impl<#generic_params> ethers_core::abi::Tokenizable for #name<#generic_args> - where - #generic_predicates - #tokenize_predicates - { - - fn from_token(token: ethers_core::abi::Token) -> Result where - Self: Sized { - if let ethers_core::abi::Token::Tuple(tokens) = token { - if tokens.len() != #params_len { - return Err(ethers_core::abi::InvalidOutputType(format!( - "Expected {} tokens, got {}: {:?}", - #params_len, - tokens.len(), - tokens - ))); - } - - let mut iter = tokens.into_iter(); + quote! { + impl<#generic_params> ethers_core::abi::Tokenizable for #name<#generic_args> + where + #generic_predicates + #tokenize_predicates + { - Ok(#init_struct_impl) - } else { - Err(ethers_core::abi::InvalidOutputType(format!( - "Expected Tuple, got {:?}", - token - ))) + fn from_token(token: ethers_core::abi::Token) -> Result where + Self: Sized { + if let ethers_core::abi::Token::Tuple(tokens) = token { + if tokens.len() != #params_len { + return Err(ethers_core::abi::InvalidOutputType(format!( + "Expected {} tokens, got {}: {:?}", + #params_len, + tokens.len(), + tokens + ))); } - } - - fn into_token(self) -> ethers_core::abi::Token { - ethers_core::abi::Token::Tuple( - vec![ - #into_token_impl - ] - ) - } + + let mut iter = tokens.into_iter(); + + Ok(#init_struct_impl) + } else { + Err(ethers_core::abi::InvalidOutputType(format!( + "Expected Tuple, got {:?}", + token + ))) + } } - }; - TokenStream::from(tokenizeable_impl) + fn into_token(self) -> ethers_core::abi::Token { + ethers_core::abi::Token::Tuple( + vec![ + #into_token_impl + ] + ) + } + } + } +} + +struct Attributes { + name: Option<(String, Span)>, + abi: Option<(String, Span)>, + signature_hash: Option<(Vec, Span)>, +} + +impl Default for Attributes { + fn default() -> Self { + Self { + name: None, + abi: None, + signature_hash: None, + } + } +} + +fn parse_attributes(input: &DeriveInput) -> Result { + let mut result = Attributes::default(); + for a in input.attrs.iter() { + if let AttrStyle::Outer = a.style { + if let Ok(Meta::List(meta)) = a.parse_meta() { + if meta.path.is_ident("ethevent") { + for n in meta.nested.iter() { + if let NestedMeta::Meta(meta) = n { + match meta { + Meta::Path(path) => { + return Err(Error::new( + path.span(), + "unrecognized ethevent parameter", + ) + .to_compile_error()); + } + Meta::List(meta) => { + // TODO support raw list + return Err(Error::new( + meta.path.span(), + "unrecognized ethevent parameter", + ) + .to_compile_error()); + } + Meta::NameValue(meta) => { + if meta.path.is_ident("name") { + if let Lit::Str(ref lit_str) = meta.lit { + if result.name.is_none() { + result.name = + Some((lit_str.value(), lit_str.span())); + } else { + return Err(Error::new( + meta.span(), + "name already specified", + ) + .to_compile_error()); + } + } else { + return Err(Error::new( + meta.span(), + "name must be a string", + ) + .to_compile_error()); + } + } else if meta.path.is_ident("abi") { + if let Lit::Str(ref lit_str) = meta.lit { + if result.abi.is_none() { + result.abi = + Some((lit_str.value(), lit_str.span())); + } else { + return Err(Error::new( + meta.span(), + "abi already specified", + ) + .to_compile_error()); + } + } else { + return Err(Error::new( + meta.span(), + "abi must be a string", + ) + .to_compile_error()); + } + } else if meta.path.is_ident("signature") { + if let Lit::Str(ref lit_str) = meta.lit { + if result.signature_hash.is_none() { + match Vec::from_hex(lit_str.value()) { + Ok(sig) => { + result.signature_hash = + Some((sig, lit_str.span())) + } + Err(err) => { + return Err(Error::new( + meta.span(), + format!( + "Expected hex signature: {:?}", + err + ), + ) + .to_compile_error()); + } + } + } else { + return Err(Error::new( + meta.span(), + "signature already specified", + ) + .to_compile_error()); + } + } else { + return Err(Error::new( + meta.span(), + "signature must be a hex string", + ) + .to_compile_error()); + } + } else { + return Err(Error::new( + meta.span(), + "unrecognized ethevent parameter", + ) + .to_compile_error()); + } + } + } + } + } + } + } + } + } + Ok(result) } diff --git a/ethers-core/src/abi/human_readable.rs b/ethers-core/src/abi/human_readable.rs index 98ec7142d..19195c6dd 100644 --- a/ethers-core/src/abi/human_readable.rs +++ b/ethers-core/src/abi/human_readable.rs @@ -152,7 +152,7 @@ impl AbiParser { } /// Parses a solidity event declaration from `event (args*) anonymous?` - fn parse_event(&self, s: &str) -> Result { + pub fn parse_event(&self, s: &str) -> Result { let mut event = s.trim(); if !event.starts_with("event ") { bail!("Not an event `{}`", s) @@ -237,7 +237,7 @@ impl AbiParser { }) } - fn parse_function(&mut self, s: &str) -> Result { + pub fn parse_function(&mut self, s: &str) -> Result { let mut input = s.trim(); if !input.starts_with("function ") { bail!("Not a function `{}`", input) @@ -328,7 +328,7 @@ impl AbiParser { } } - fn parse_constructor(&self, s: &str) -> Result { + pub fn parse_constructor(&self, s: &str) -> Result { let mut input = s.trim(); if !input.starts_with("constructor") { bail!("Not a constructor `{}`", input) From 71e3b238d876d4e8b66407cff0fcbbfe96a9fdce Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sun, 14 Mar 2021 22:11:39 +0100 Subject: [PATCH 11/12] test: add proc macro derive tests --- ethers-contract/tests/common/derive.rs | 124 ++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 11 deletions(-) diff --git a/ethers-contract/tests/common/derive.rs b/ethers-contract/tests/common/derive.rs index ac0fcf14b..4f1d30546 100644 --- a/ethers-contract/tests/common/derive.rs +++ b/ethers-contract/tests/common/derive.rs @@ -1,26 +1,27 @@ -use ethers_contract::EthAbiType; +use ethers::core::types::{H160, H256, I256, U128, U256}; +use ethers_contract::{EthAbiType, EthEvent}; use ethers_core::abi::Tokenizable; use ethers_core::types::Address; #[derive(Debug, Clone, PartialEq, EthAbiType)] -pub struct ValueChanged { - pub old_author: Address, - pub new_author: Address, - pub old_value: String, - pub new_value: String, +struct ValueChanged { + old_author: Address, + new_author: Address, + old_value: String, + new_value: String, } #[derive(Debug, Clone, PartialEq, EthAbiType)] -pub struct ValueChangedWrapper { - pub inner: ValueChanged, - pub msg: String, +struct ValueChangedWrapper { + inner: ValueChanged, + msg: String, } #[derive(Debug, Clone, PartialEq, EthAbiType)] -pub struct ValueChangedTuple(Address, Address, String, String); +struct ValueChangedTuple(Address, Address, String, String); #[derive(Debug, Clone, PartialEq, EthAbiType)] -pub struct ValueChangedTupleWrapper(ValueChangedTuple, String); +struct ValueChangedTupleWrapper(ValueChangedTuple, String); #[test] fn can_detokenize_struct() { @@ -79,3 +80,104 @@ fn can_detokenize_nested_tuple_struct() { let token = value.clone().into_token(); assert_eq!(value, ValueChangedTupleWrapper::from_token(token).unwrap()); } + +#[test] +fn can_derive_eth_event() { + #[derive(Debug, Clone, PartialEq, EthEvent)] + pub struct ValueChangedEvent { + old_author: Address, + new_author: Address, + old_value: String, + new_value: String, + } + + let value = ValueChangedEvent { + old_author: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap(), + new_author: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse().unwrap(), + old_value: "50".to_string(), + new_value: "100".to_string(), + }; + + assert_eq!("ValueChangedEvent", value.name()); + assert_eq!( + "ValueChangedEvent(address,address,string,string)", + ValueChangedEvent::abi_signature() + ); + + let token = value.clone().into_token(); + assert_eq!(value, ValueChangedEvent::from_token(token).unwrap()); +} + +#[test] +fn can_set_eth_event_name_attribute() { + #[derive(Debug, PartialEq, EthEvent)] + #[ethevent(name = "MyEvent")] + pub struct ValueChangedEvent { + old_author: Address, + new_author: Address, + old_value: String, + new_value: String, + } + + let value = ValueChangedEvent { + old_author: "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap(), + new_author: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse().unwrap(), + old_value: "50".to_string(), + new_value: "100".to_string(), + }; + + assert_eq!("MyEvent", value.name()); + assert_eq!( + "MyEvent(address,address,string,string)", + ValueChangedEvent::abi_signature() + ); +} + +#[test] +fn can_detect_various_event_abi_types() { + #[derive(Debug, PartialEq, EthEvent)] + struct ValueChangedEvent { + old_author: Address, + s: String, + h1: H256, + i256: I256, + u256: U256, + b: bool, + v: Vec
, + bs: Vec, + h160: H160, + u128: U128, + int8: i8, + int16: i16, + int32: i32, + int64: i64, + int128: i128, + uint8: u8, + uint16: u16, + uint32: u32, + uint64: u64, + uint128: u128, + } + + assert_eq!( + "ValueChangedEvent(address,string,bytes32,int256,uint256,bool,address[],bool[],bytes20,uint128,int8,int16,int32,int64,int128,uint8,uint16,uint32,uint64,uint128)", + ValueChangedEvent::abi_signature() + ); +} + +// #[test] +// fn can_set_eth_abi_attribute() { +// #[derive(Debug, Clone, PartialEq, EthAbiType)] +// struct SomeType { +// inner: Address, +// msg: String, +// } +// +// #[derive(Debug, PartialEq, EthEvent)] +// #[ethevent(abi = "ValueChangedEvent(address,(address,string),string)")] +// pub struct ValueChangedEvent { +// old_author: Address, +// inner: SomeType, +// new_value: String, +// } +// } \ No newline at end of file From 54096e6a50a3ce67a1116faa31acf0e47b6d01a0 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Mon, 15 Mar 2021 00:40:13 +0100 Subject: [PATCH 12/12] chore: rustfmt --- ethers-contract/src/event.rs | 2 +- ethers-contract/tests/common/derive.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs index da659217d..73493fd75 100644 --- a/ethers-contract/src/event.rs +++ b/ethers-contract/src/event.rs @@ -5,8 +5,8 @@ use ethers_core::{ types::{BlockNumber, Filter, Log, TxHash, ValueOrArray, H256, U64}, }; use ethers_providers::{FilterWatcher, Middleware, PubsubClient, SubscriptionStream}; -use std::marker::PhantomData; use std::borrow::Cow; +use std::marker::PhantomData; /// A trait for implementing event bindings pub trait EthEvent: Detokenize { diff --git a/ethers-contract/tests/common/derive.rs b/ethers-contract/tests/common/derive.rs index 4f1d30546..4f36b5050 100644 --- a/ethers-contract/tests/common/derive.rs +++ b/ethers-contract/tests/common/derive.rs @@ -180,4 +180,4 @@ fn can_detect_various_event_abi_types() { // inner: SomeType, // new_value: String, // } -// } \ No newline at end of file +// }