From 790550c1e51762cd57c58868e05247468d12a68a Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Mon, 9 May 2022 19:46:43 +0200 Subject: [PATCH] feat(abi): add human readable tokenizer and parser (#1234) * feat(abi): add human readable tokenizer and parser * feat(abi): support nested tuples --- Cargo.lock | 1 + ethers-core/Cargo.toml | 1 + ethers-core/src/abi/error.rs | 10 +- ethers-core/src/abi/human_readable/lexer.rs | 849 ++++++++++++++++++ .../mod.rs} | 1 + ethers-core/src/abi/mod.rs | 4 +- 6 files changed, 862 insertions(+), 4 deletions(-) create mode 100644 ethers-core/src/abi/human_readable/lexer.rs rename ethers-core/src/abi/{human_readable.rs => human_readable/mod.rs} (99%) diff --git a/Cargo.lock b/Cargo.lock index 2545ac20..d9704a4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1262,6 +1262,7 @@ dependencies = [ "syn", "thiserror", "tiny-keccak", + "unicode-xid", ] [[package]] diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml index be4ec172..35fa57d7 100644 --- a/ethers-core/Cargo.toml +++ b/ethers-core/Cargo.toml @@ -29,6 +29,7 @@ thiserror = { version = "1.0.31", default-features = false } bytes = { version = "1.1.0", features = ["serde"] } hex = { version = "0.4.3", default-features = false, features = ["std"] } once_cell = { version = "1.10.0", optional = true } +unicode-xid = "0.2.2" # macros feature enabled dependencies cargo_metadata = { version = "0.14.2", optional = true } diff --git a/ethers-core/src/abi/error.rs b/ethers-core/src/abi/error.rs index 48a3dcd2..bd54c0a7 100644 --- a/ethers-core/src/abi/error.rs +++ b/ethers-core/src/abi/error.rs @@ -1,5 +1,5 @@ //! Boilerplate error definitions. -use crate::abi::InvalidOutputType; +use crate::abi::{human_readable, InvalidOutputType}; use thiserror::Error; /// A type alias for std's Result with the Error as our error type. @@ -10,8 +10,12 @@ pub type Result = std::result::Result; pub enum ParseError { #[error("{0}")] Message(String), + // ethabi parser error #[error(transparent)] - ParseError(#[from] super::Error), + ParseError(#[from] ethabi::Error), + // errors from human readable lexer + #[error(transparent)] + LexerError(#[from] human_readable::lexer::LexerError), } macro_rules! _format_err { @@ -32,7 +36,7 @@ pub(crate) use _bail as bail; pub enum AbiError { /// Thrown when the ABI decoding fails #[error(transparent)] - DecodingError(#[from] crate::abi::Error), + DecodingError(#[from] ethabi::Error), /// Thrown when detokenizing an argument #[error(transparent)] diff --git a/ethers-core/src/abi/human_readable/lexer.rs b/ethers-core/src/abi/human_readable/lexer.rs new file mode 100644 index 00000000..907f39e3 --- /dev/null +++ b/ethers-core/src/abi/human_readable/lexer.rs @@ -0,0 +1,849 @@ +use ethabi::{Event, EventParam, ParamType}; +use std::{fmt, iter::Peekable, str::CharIndices}; +use unicode_xid::UnicodeXID; + +pub type Spanned = Result<(Loc, Token, Loc), Error>; + +macro_rules! unrecognised { + ($l:ident,$r:ident,$t:expr) => { + return Err(LexerError::UnrecognisedToken($l, $r, $t)) + }; +} + +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum Token<'input> { + Identifier(&'input str), + Number(&'input str), + HexNumber(&'input str), + // Punctuation + OpenParenthesis, + CloseParenthesis, + Comma, + OpenBracket, + CloseBracket, + Semicolon, + Point, + + Struct, + Event, + Error, + Enum, + Function, + Tuple, + + Memory, + Storage, + Calldata, + + Public, + Private, + Internal, + External, + + Constant, + + Type, + Pure, + View, + Payable, + Returns, + Anonymous, + Receive, + Fallback, + Abstract, + Virtual, + Override, + + Constructor, + Indexed, + + Uint(usize), + Int(usize), + Bytes(usize), + // prior to 0.8.0 `byte` used to be an alias for `bytes1` + Byte, + DynamicBytes, + Bool, + Address, + String, +} + +// === impl Token === + +impl<'input> Token<'input> { + fn into_param_type(self) -> Option { + let param = match self { + Token::Uint(size) => ParamType::Uint(size), + Token::Int(size) => ParamType::Int(size), + Token::Bytes(size) => ParamType::FixedBytes(size), + Token::Byte => ParamType::FixedBytes(1), + Token::DynamicBytes => ParamType::Bytes, + Token::Bool => ParamType::Bool, + Token::Address => ParamType::Address, + Token::String => ParamType::String, + _ => return None, + }; + + Some(param) + } +} + +impl<'input> fmt::Display for Token<'input> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Token::Identifier(id) => write!(f, "{}", id), + Token::Number(num) => write!(f, "{}", num), + Token::HexNumber(num) => write!(f, "0x{}", num), + Token::Uint(w) => write!(f, "uint{}", w), + Token::Int(w) => write!(f, "int{}", w), + Token::Bytes(w) => write!(f, "bytes{}", w), + Token::Byte => write!(f, "byte"), + Token::DynamicBytes => write!(f, "bytes"), + Token::Semicolon => write!(f, ";"), + Token::Comma => write!(f, ","), + Token::OpenParenthesis => write!(f, "("), + Token::CloseParenthesis => write!(f, ")"), + Token::OpenBracket => write!(f, "["), + Token::CloseBracket => write!(f, "]"), + Token::Point => write!(f, "."), + Token::Tuple => write!(f, "tuple"), + Token::Bool => write!(f, "bool"), + Token::Address => write!(f, "address"), + Token::String => write!(f, "string"), + Token::Function => write!(f, "function"), + Token::Struct => write!(f, "struct"), + Token::Event => write!(f, "event"), + Token::Error => write!(f, "error"), + Token::Enum => write!(f, "enum"), + Token::Type => write!(f, "type"), + Token::Memory => write!(f, "memory"), + Token::Storage => write!(f, "storage"), + Token::Calldata => write!(f, "calldata"), + Token::Public => write!(f, "public"), + Token::Private => write!(f, "private"), + Token::Internal => write!(f, "internal"), + Token::External => write!(f, "external"), + Token::Constant => write!(f, "constant"), + Token::Pure => write!(f, "pure"), + Token::View => write!(f, "view"), + Token::Payable => write!(f, "payable"), + Token::Returns => write!(f, "returns"), + Token::Anonymous => write!(f, "anonymous"), + Token::Constructor => write!(f, "constructor"), + Token::Indexed => write!(f, "indexed"), + Token::Receive => write!(f, "receive"), + Token::Fallback => write!(f, "fallback"), + Token::Abstract => write!(f, "abstract"), + Token::Virtual => write!(f, "virtual"), + Token::Override => write!(f, "override"), + } + } +} + +#[derive(Debug, PartialEq, Clone, thiserror::Error)] +pub enum LexerError { + #[error("UnrecognisedToken {0}:{1} `{2}`")] + UnrecognisedToken(usize, usize, String), + #[error("Expected token `{2}` at {0}:{1} ")] + ExpectedToken(usize, usize, String), + #[error("EndofFileInHex {0}:{1}")] + EndofFileInHex(usize, usize), + #[error("MissingNumber {0}:{1}")] + MissingNumber(usize, usize), + #[error("end of file but expected `{0}`")] + EndOfFileExpectedToken(String), + #[error("end of file")] + EndOfFile, +} + +#[derive(Clone, Debug)] +pub(crate) struct HumanReadableLexer<'input> { + input: &'input str, + chars: Peekable>, +} + +// === impl HumanReadableLexer === + +impl<'input> HumanReadableLexer<'input> { + /// Creates a new instance of the lexer + pub fn new(input: &'input str) -> Self { + Self { chars: input.char_indices().peekable(), input } + } + + fn next_token(&mut self) -> Option, usize, LexerError>> { + loop { + match self.chars.next() { + Some((start, ch)) if UnicodeXID::is_xid_start(ch) || ch == '_' => { + let end; + loop { + if let Some((i, ch)) = self.chars.peek() { + if !UnicodeXID::is_xid_continue(*ch) && *ch != '$' { + end = *i; + break + } + self.chars.next(); + } else { + end = self.input.len(); + break + } + } + let id = &self.input[start..end]; + + return if let Some(w) = keyword(id) { + Some(Ok((start, w, end))) + } else { + Some(Ok((start, Token::Identifier(id), end))) + } + } + Some((start, ch)) if ch.is_ascii_digit() => { + let mut end = start + 1; + if ch == '0' { + if let Some((_, 'x')) = self.chars.peek() { + // hex number + self.chars.next(); + + let mut end = match self.chars.next() { + Some((end, ch)) if ch.is_ascii_hexdigit() => end, + Some((_, _)) => { + return Some(Err(LexerError::MissingNumber(start, start + 1))) + } + None => { + return Some(Err(LexerError::EndofFileInHex( + start, + self.input.len(), + ))) + } + }; + + while let Some((i, ch)) = self.chars.peek() { + if !ch.is_ascii_hexdigit() && *ch != '_' { + break + } + end = *i; + self.chars.next(); + } + + return Some(Ok(( + start, + Token::HexNumber(&self.input[start..=end]), + end + 1, + ))) + } + } + + loop { + if let Some((i, ch)) = self.chars.peek().cloned() { + if !ch.is_ascii_digit() { + break + } + self.chars.next(); + end = i + 1; + } else { + end = self.input.len(); + break + } + } + return Some(Ok((start, Token::Number(&self.input[start..end]), end + 1))) + } + Some((i, '(')) => return Some(Ok((i, Token::OpenParenthesis, i + 1))), + Some((i, ')')) => return Some(Ok((i, Token::CloseParenthesis, i + 1))), + Some((i, ';')) => return Some(Ok((i, Token::Semicolon, i + 1))), + Some((i, ',')) => return Some(Ok((i, Token::Comma, i + 1))), + Some((i, '.')) => return Some(Ok((i, Token::Point, i + 1))), + Some((i, '[')) => return Some(Ok((i, Token::OpenBracket, i + 1))), + Some((i, ']')) => return Some(Ok((i, Token::CloseBracket, i + 1))), + Some((_, ch)) if ch.is_whitespace() => (), + Some((start, _)) => { + let mut end; + loop { + if let Some((i, ch)) = self.chars.next() { + end = i; + if ch.is_whitespace() { + break + } + } else { + end = self.input.len(); + break + } + } + + return Some(Err(LexerError::UnrecognisedToken( + start, + end, + self.input[start..end].to_owned(), + ))) + } + None => return None, + } + } + } +} + +impl<'input> Iterator for HumanReadableLexer<'input> { + type Item = Spanned, usize, LexerError>; + + /// Return the next token + fn next(&mut self) -> Option { + self.next_token() + } +} + +#[derive(Clone, Debug)] +pub struct HumanReadableParser<'input> { + lexer: Peekable>, +} + +// === impl HumanReadableParser === + +impl<'input> HumanReadableParser<'input> { + /// Creates a new instance of the lexer + pub fn new(input: &'input str) -> Self { + let lexer = HumanReadableLexer::new(input); + Self { lexer: lexer.peekable() } + } + + /// Parses the input into a [ParamType] + pub fn parse_type(input: &'input str) -> Result { + Self::new(input).take_param() + } + + /// Parses an [Event] from a human readable form + /// + /// # Example + /// + /// ``` + /// use ethers_core::abi::HumanReadableParser; + /// let mut event = HumanReadableParser::parse_event("event ValueChanged(address indexed author, string oldValue, string newValue)").unwrap(); + /// ``` + pub fn parse_event(input: &'input str) -> Result { + Self::new(input).take_event() + } + + pub fn take_event(&mut self) -> Result { + let (l, token, r) = self.next_spanned()?; + let name = match token { + Token::Event => { + let (_, next, _) = self.lexer.peek().cloned().ok_or(LexerError::EndOfFile)??; + if let Token::Identifier(name) = next { + self.next(); + name + } else { + "" + } + } + Token::Identifier(name) => name, + t => unrecognised!(l, r, t.to_string()), + }; + + self.take_open_parenthesis()?; + let inputs = self.take_event_params()?; + self.take_close_parenthesis()?; + let event = Event { name: name.to_string(), inputs, anonymous: self.take_anonymous() }; + + Ok(event) + } + + fn take_anonymous(&mut self) -> bool { + if self.peek_next(Token::Anonymous) { + self.next(); + true + } else { + false + } + } + + /// Parses all event params + fn take_event_params(&mut self) -> Result, LexerError> { + let mut params = Vec::new(); + + if self.peek_next(Token::CloseParenthesis) { + return Ok(params) + } + + loop { + params.push(self.take_event_param()?); + + let (l, next, r) = match self.peek() { + Some(next) => next?, + _ => break, + }; + + match next { + Token::Comma => { + self.next_spanned()?; + } + Token::CloseParenthesis => break, + t => unrecognised!(l, r, t.to_string()), + } + } + Ok(params) + } + + fn take_event_param(&mut self) -> Result { + let kind = self.take_param()?; + let mut name = ""; + let mut indexed = false; + + loop { + let (_, token, _) = self.peek_some()?; + match token { + Token::Indexed => { + indexed = true; + self.next(); + } + Token::Identifier(id) => { + name = id; + self.next(); + break + } + _ => break, + }; + } + Ok(EventParam { name: name.to_string(), kind, indexed }) + } + + /// Parses a list of parameter types + fn take_params(&mut self) -> Result, LexerError> { + let mut params = Vec::new(); + + if self.peek_next(Token::CloseParenthesis) { + return Ok(params) + } + loop { + params.push(self.take_param()?); + + let (l, next, r) = match self.peek() { + Some(next) => next?, + _ => break, + }; + match next { + Token::Comma => { + self.next_spanned()?; + } + Token::CloseParenthesis => break, + t => unrecognised!(l, r, t.to_string()), + } + } + + Ok(params) + } + + fn take_param(&mut self) -> Result { + let (l, token, r) = self.next_spanned()?; + let kind = match token { + Token::OpenParenthesis => { + let ty = self.take_params()?; + self.take_next_exact(Token::CloseParenthesis)?; + ParamType::Tuple(ty) + } + t => t + .into_param_type() + .ok_or_else(|| LexerError::UnrecognisedToken(l, r, t.to_string()))?, + }; + self.take_array_tail(kind) + } + + fn take_array_tail(&mut self, kind: ParamType) -> Result { + let (_, token, _) = match self.peek() { + Some(next) => next?, + _ => return Ok(kind), + }; + + match token { + Token::OpenBracket => { + self.next_spanned()?; + let (_, token, _) = self.peek_some()?; + let kind = if let Token::Number(size) = token { + self.next_spanned()?; + ParamType::FixedArray(Box::new(kind), size.parse().unwrap()) + } else { + ParamType::Array(Box::new(kind)) + }; + self.take_next_exact(Token::CloseBracket)?; + self.take_array_tail(kind) + } + _ => Ok(kind), + } + } + + fn take_open_parenthesis(&mut self) -> Result<(), LexerError> { + self.take_next_exact(Token::OpenParenthesis) + } + + fn take_close_parenthesis(&mut self) -> Result<(), LexerError> { + self.take_next_exact(Token::CloseParenthesis) + } + + fn take_next_exact(&mut self, token: Token) -> Result<(), LexerError> { + let (l, next, r) = self.next_spanned().map_err(|err| match err { + LexerError::UnrecognisedToken(l, r, _) => { + LexerError::ExpectedToken(l, r, token.to_string()) + } + LexerError::EndOfFile => LexerError::EndOfFileExpectedToken(token.to_string()), + err => err, + })?; + if next != token { + unrecognised!(l, r, next.to_string()) + } + Ok(()) + } + + /// Returns true if the next token is the given `token` + fn peek_next(&mut self, token: Token) -> bool { + if let Some(Ok(next)) = self.lexer.peek() { + next.1 == token + } else { + false + } + } + + fn next_spanned(&mut self) -> Spanned, usize, LexerError> { + self.next().ok_or(LexerError::EndOfFile)? + } + + fn next(&mut self) -> Option, usize, LexerError>> { + self.lexer.next() + } + + fn peek(&mut self) -> Option, usize, LexerError>> { + self.lexer.peek().cloned() + } + + fn peek_some(&mut self) -> Spanned, usize, LexerError> { + self.lexer.peek().cloned().ok_or(LexerError::EndOfFile)? + } +} + +fn keyword(id: &str) -> Option { + let token = match id { + "address" => Token::Address, + "anonymous" => Token::Anonymous, + "bool" => Token::Bool, + "bytes1" => Token::Bytes(1), + "bytes2" => Token::Bytes(2), + "bytes3" => Token::Bytes(3), + "bytes4" => Token::Bytes(4), + "bytes5" => Token::Bytes(5), + "bytes6" => Token::Bytes(6), + "bytes7" => Token::Bytes(7), + "bytes8" => Token::Bytes(8), + "bytes9" => Token::Bytes(9), + "bytes10" => Token::Bytes(10), + "bytes11" => Token::Bytes(11), + "bytes12" => Token::Bytes(12), + "bytes13" => Token::Bytes(13), + "bytes14" => Token::Bytes(14), + "bytes15" => Token::Bytes(15), + "bytes16" => Token::Bytes(16), + "bytes17" => Token::Bytes(17), + "bytes18" => Token::Bytes(18), + "bytes19" => Token::Bytes(19), + "bytes20" => Token::Bytes(20), + "bytes21" => Token::Bytes(21), + "bytes22" => Token::Bytes(22), + "bytes23" => Token::Bytes(23), + "bytes24" => Token::Bytes(24), + "bytes25" => Token::Bytes(25), + "bytes26" => Token::Bytes(26), + "bytes27" => Token::Bytes(27), + "bytes28" => Token::Bytes(28), + "bytes29" => Token::Bytes(29), + "bytes30" => Token::Bytes(30), + "bytes31" => Token::Bytes(31), + "bytes32" => Token::Bytes(32), + "bytes" => Token::DynamicBytes, + "byte" => Token::Byte, + "calldata" => Token::Calldata, + "constant" => Token::Constant, + "constructor" => Token::Constructor, + "enum" => Token::Enum, + "event" => Token::Event, + "error" => Token::Error, + "external" => Token::External, + "function" => Token::Function, + "indexed" => Token::Indexed, + "tuple" => Token::Tuple, + "int8" => Token::Int(8), + "int16" => Token::Int(16), + "int24" => Token::Int(24), + "int32" => Token::Int(32), + "int40" => Token::Int(40), + "int48" => Token::Int(48), + "int56" => Token::Int(56), + "int64" => Token::Int(64), + "int72" => Token::Int(72), + "int80" => Token::Int(80), + "int88" => Token::Int(88), + "int96" => Token::Int(96), + "int104" => Token::Int(104), + "int112" => Token::Int(112), + "int120" => Token::Int(120), + "int128" => Token::Int(128), + "int136" => Token::Int(136), + "int144" => Token::Int(144), + "int152" => Token::Int(152), + "int160" => Token::Int(160), + "int168" => Token::Int(168), + "int176" => Token::Int(176), + "int184" => Token::Int(184), + "int192" => Token::Int(192), + "int200" => Token::Int(200), + "int208" => Token::Int(208), + "int216" => Token::Int(216), + "int224" => Token::Int(224), + "int232" => Token::Int(232), + "int240" => Token::Int(240), + "int248" => Token::Int(248), + "int256" => Token::Int(256), + "internal" => Token::Internal, + "int" => Token::Int(256), + "memory" => Token::Memory, + "payable" => Token::Payable, + "private" => Token::Private, + "public" => Token::Public, + "pure" => Token::Pure, + "returns" => Token::Returns, + "storage" => Token::Storage, + "string" => Token::String, + "struct" => Token::Struct, + "type" => Token::Type, + "uint8" => Token::Uint(8), + "uint16" => Token::Uint(16), + "uint24" => Token::Uint(24), + "uint32" => Token::Uint(32), + "uint40" => Token::Uint(40), + "uint48" => Token::Uint(48), + "uint56" => Token::Uint(56), + "uint64" => Token::Uint(64), + "uint72" => Token::Uint(72), + "uint80" => Token::Uint(80), + "uint88" => Token::Uint(88), + "uint96" => Token::Uint(96), + "uint104" => Token::Uint(104), + "uint112" => Token::Uint(112), + "uint120" => Token::Uint(120), + "uint128" => Token::Uint(128), + "uint136" => Token::Uint(136), + "uint144" => Token::Uint(144), + "uint152" => Token::Uint(152), + "uint160" => Token::Uint(160), + "uint168" => Token::Uint(168), + "uint176" => Token::Uint(176), + "uint184" => Token::Uint(184), + "uint192" => Token::Uint(192), + "uint200" => Token::Uint(200), + "uint208" => Token::Uint(208), + "uint216" => Token::Uint(216), + "uint224" => Token::Uint(224), + "uint232" => Token::Uint(232), + "uint240" => Token::Uint(240), + "uint248" => Token::Uint(248), + "uint256" => Token::Uint(256), + "uint" => Token::Uint(256), + "view" => Token::View, + "receive" => Token::Receive, + "fallback" => Token::Fallback, + "abstract" => Token::Abstract, + "virtual" => Token::Virtual, + "override" => Token::Override, + _ => return None, + }; + Some(token) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_param() { + assert_eq!(HumanReadableParser::parse_type("address").unwrap(), ParamType::Address); + assert_eq!(HumanReadableParser::parse_type("bytes").unwrap(), ParamType::Bytes); + assert_eq!(HumanReadableParser::parse_type("bytes32").unwrap(), ParamType::FixedBytes(32)); + assert_eq!(HumanReadableParser::parse_type("bool").unwrap(), ParamType::Bool); + assert_eq!(HumanReadableParser::parse_type("string").unwrap(), ParamType::String); + assert_eq!(HumanReadableParser::parse_type("int").unwrap(), ParamType::Int(256)); + assert_eq!(HumanReadableParser::parse_type("uint").unwrap(), ParamType::Uint(256)); + assert_eq!( + HumanReadableParser::parse_type( + " + int32" + ) + .unwrap(), + ParamType::Int(32) + ); + assert_eq!(HumanReadableParser::parse_type("uint32").unwrap(), ParamType::Uint(32)); + } + + #[test] + fn test_parse_array_param() { + assert_eq!( + HumanReadableParser::parse_type("address[]").unwrap(), + ParamType::Array(Box::new(ParamType::Address)) + ); + assert_eq!( + HumanReadableParser::parse_type("uint[]").unwrap(), + ParamType::Array(Box::new(ParamType::Uint(256))) + ); + assert_eq!( + HumanReadableParser::parse_type("bytes[]").unwrap(), + ParamType::Array(Box::new(ParamType::Bytes)) + ); + assert_eq!( + HumanReadableParser::parse_type("bool[][]").unwrap(), + ParamType::Array(Box::new(ParamType::Array(Box::new(ParamType::Bool)))) + ); + } + + #[test] + fn test_parse_fixed_array_param() { + assert_eq!( + HumanReadableParser::parse_type("address[2]").unwrap(), + ParamType::FixedArray(Box::new(ParamType::Address), 2) + ); + assert_eq!( + HumanReadableParser::parse_type("bool[17]").unwrap(), + ParamType::FixedArray(Box::new(ParamType::Bool), 17) + ); + assert_eq!( + HumanReadableParser::parse_type("bytes[45][3]").unwrap(), + ParamType::FixedArray( + Box::new(ParamType::FixedArray(Box::new(ParamType::Bytes), 45)), + 3 + ) + ); + } + + #[test] + fn test_parse_mixed_arrays() { + assert_eq!( + HumanReadableParser::parse_type("bool[][3]").unwrap(), + ParamType::FixedArray(Box::new(ParamType::Array(Box::new(ParamType::Bool))), 3) + ); + assert_eq!( + HumanReadableParser::parse_type("bool[3][]").unwrap(), + ParamType::Array(Box::new(ParamType::FixedArray(Box::new(ParamType::Bool), 3))) + ); + } + + #[test] + fn test_parse_struct_param() { + assert_eq!( + HumanReadableParser::parse_type("(address,bool)").unwrap(), + ParamType::Tuple(vec![ParamType::Address, ParamType::Bool]) + ); + assert_eq!( + HumanReadableParser::parse_type("(bool[3],uint256)").unwrap(), + ParamType::Tuple(vec![ + ParamType::FixedArray(Box::new(ParamType::Bool), 3), + ParamType::Uint(256) + ]) + ); + } + + #[test] + fn test_parse_nested_struct_param() { + assert_eq!( + HumanReadableParser::parse_type("(address,bool,(bool,uint256))").unwrap(), + ParamType::Tuple(vec![ + ParamType::Address, + ParamType::Bool, + ParamType::Tuple(vec![ParamType::Bool, ParamType::Uint(256)]) + ]) + ); + } + + #[test] + fn test_parse_complex_nested_struct_param() { + assert_eq!( + HumanReadableParser::parse_type( + "(address,bool,(bool,uint256,(bool,uint256)),(bool,uint256))" + ) + .unwrap(), + ParamType::Tuple(vec![ + ParamType::Address, + ParamType::Bool, + ParamType::Tuple(vec![ + ParamType::Bool, + ParamType::Uint(256), + ParamType::Tuple(vec![ParamType::Bool, ParamType::Uint(256)]) + ]), + ParamType::Tuple(vec![ParamType::Bool, ParamType::Uint(256)]) + ]) + ); + } + + #[test] + fn test_parse_nested_tuple_array_param() { + assert_eq!( + HumanReadableParser::parse_type("(uint256,bytes32)[]").unwrap(), + ParamType::Array(Box::new(ParamType::Tuple(vec![ + ParamType::Uint(256), + ParamType::FixedBytes(32) + ]))) + ) + } + + #[test] + fn test_parse_inner_tuple_array_param() { + let abi = "((uint256,bytes32)[],address)"; + let read = HumanReadableParser::parse_type(abi).unwrap(); + + let param = ParamType::Tuple(vec![ + ParamType::Array(Box::new(ParamType::Tuple(vec![ + ParamType::Uint(256), + ParamType::FixedBytes(32), + ]))), + ParamType::Address, + ]); + assert_eq!(read, param); + } + + #[test] + fn test_parse_complex_tuple_array_param() { + let abi = "((uint256,uint256)[],(uint256,(uint256,uint256))[])"; + let read = HumanReadableParser::parse_type(abi).unwrap(); + let param = ParamType::Tuple(vec![ + ParamType::Array(Box::new(ParamType::Tuple(vec![ + ParamType::Uint(256), + ParamType::Uint(256), + ]))), + ParamType::Array(Box::new(ParamType::Tuple(vec![ + ParamType::Uint(256), + ParamType::Tuple(vec![ParamType::Uint(256), ParamType::Uint(256)]), + ]))), + ]); + assert_eq!(read, param); + } + + #[test] + fn test_parse_event() { + let abi = "event ValueChanged(address indexed author, string oldValue, string newValue)"; + let event = HumanReadableParser::parse_event(abi).unwrap(); + + assert_eq!( + Event { + name: "ValueChanged".to_string(), + inputs: vec![ + EventParam { + name: "author".to_string(), + kind: ParamType::Address, + indexed: true + }, + EventParam { + name: "oldValue".to_string(), + kind: ParamType::String, + indexed: false + }, + EventParam { + name: "newValue".to_string(), + kind: ParamType::String, + indexed: false + } + ], + anonymous: false + }, + event + ); + } +} diff --git a/ethers-core/src/abi/human_readable.rs b/ethers-core/src/abi/human_readable/mod.rs similarity index 99% rename from ethers-core/src/abi/human_readable.rs rename to ethers-core/src/abi/human_readable/mod.rs index 349b520b..65af6f65 100644 --- a/ethers-core/src/abi/human_readable.rs +++ b/ethers-core/src/abi/human_readable/mod.rs @@ -6,6 +6,7 @@ use crate::abi::{ struct_def::{FieldType, StructFieldType}, Abi, Constructor, Event, EventParam, Function, Param, ParamType, SolStruct, StateMutability, }; +pub mod lexer; /// A parser that turns a "human readable abi" into a `Abi` pub struct AbiParser { diff --git a/ethers-core/src/abi/mod.rs b/ethers-core/src/abi/mod.rs index b4db858a..196b20a0 100644 --- a/ethers-core/src/abi/mod.rs +++ b/ethers-core/src/abi/mod.rs @@ -20,7 +20,9 @@ mod error; pub use error::{AbiError, ParseError}; mod human_readable; -pub use human_readable::{parse as parse_abi, parse_str as parse_abi_str, AbiParser}; +pub use human_readable::{ + lexer::HumanReadableParser, parse as parse_abi, parse_str as parse_abi_str, AbiParser, +}; use crate::types::{H256, H512, I256, U128, U256, U64};