From eb26915081e62d10711ab24eb9edac3e170cebaa Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Thu, 29 Oct 2020 09:48:24 +0200 Subject: [PATCH] feat: allow encoding/decoding function data (#90) * feat: allow encoding/decoding function data * feat: allow decoding event data * feat: human readable abi inspired from https://blog.ricmoo.com/human-readable-contract-abis-in-ethers-js-141902f4d917 * test: add event / fn decoding tests * chore: fix clippy * feat(abigen): allow providing args in human readable format --- .../ethers-contract-abigen/src/contract.rs | 31 +- .../src/contract/common.rs | 25 +- ethers-contract/src/base.rs | 191 +++++++++++- ethers-contract/src/call.rs | 13 +- ethers-contract/src/contract.rs | 19 +- ethers-contract/src/event.rs | 19 +- ethers-core/src/abi/human_readable.rs | 291 ++++++++++++++++++ ethers-core/src/abi/mod.rs | 3 + ethers/examples/contract.rs | 7 +- 9 files changed, 557 insertions(+), 42 deletions(-) create mode 100644 ethers-core/src/abi/human_readable.rs diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs index 277f5150..be8fea6b 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract.rs @@ -7,7 +7,10 @@ mod types; use super::util; use super::Abigen; use anyhow::{anyhow, Context as _, Result}; -use ethers_core::{abi::Abi, types::Address}; +use ethers_core::{ + abi::{parse_abi, Abi}, + types::Address, +}; use inflector::Inflector; use proc_macro2::{Ident, Literal, TokenStream}; use quote::quote; @@ -22,6 +25,9 @@ pub(crate) struct Context { /// The parsed ABI. abi: Abi, + /// Was the ABI in human readable format? + human_readable: bool, + /// The contract name as an identifier. contract_name: Ident, @@ -92,13 +98,23 @@ impl Context { fn from_abigen(args: Abigen) -> Result { // get the actual ABI string let abi_str = args.abi_source.get().context("failed to get ABI JSON")?; - // parse it - let abi: Abi = serde_json::from_str(&abi_str) - .with_context(|| format!("invalid artifact JSON '{}'", abi_str)) - .with_context(|| { - format!("failed to parse artifact from source {:?}", args.abi_source,) - })?; + let (abi, human_readable): (Abi, _) = if let Ok(abi) = serde_json::from_str(&abi_str) { + // normal abi format + (abi, false) + } else { + // heuristic for parsing the human readable format + + // replace bad chars + let abi_str = abi_str.replace('[', "").replace(']', "").replace(',', ""); + // split lines and get only the non-empty things + let split: Vec<&str> = abi_str + .split('\n') + .map(|x| x.trim()) + .filter(|x| !x.is_empty()) + .collect(); + (parse_abi(&split)?, true) + }; let contract_name = util::ident(&args.contract_name); @@ -125,6 +141,7 @@ impl Context { Ok(Context { abi, + human_readable, abi_str: Literal::string(&abi_str), contract_name, method_aliases, diff --git a/ethers-contract/ethers-contract-abigen/src/contract/common.rs b/ethers-contract/ethers-contract-abigen/src/contract/common.rs index 5fa9dcee..38633fdc 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/common.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/common.rs @@ -15,7 +15,7 @@ pub(crate) fn imports(name: &str) -> TokenStream { use std::sync::Arc; use ethers::{ core::{ - abi::{Abi, Token, Detokenize, InvalidOutputType, Tokenizable}, + abi::{Abi, Token, Detokenize, InvalidOutputType, Tokenizable, parse_abi}, types::*, // import all the types so that we can codegen for everything }, contract::{Contract, builders::{ContractCall, Event}, Lazy}, @@ -28,10 +28,29 @@ pub(crate) fn struct_declaration(cx: &Context, abi_name: &proc_macro2::Ident) -> let name = &cx.contract_name; let abi = &cx.abi_str; + let abi_parse = if !cx.human_readable { + quote! { + pub static #abi_name: Lazy = Lazy::new(|| serde_json::from_str(#abi) + .expect("invalid abi")); + } + } else { + quote! { + pub static #abi_name: Lazy = Lazy::new(|| { + let abi_str = #abi.replace('[', "").replace(']', "").replace(',', ""); + // split lines and get only the non-empty things + let split: Vec<&str> = abi_str + .split("\n") + .map(|x| x.trim()) + .filter(|x| !x.is_empty()) + .collect(); + parse_abi(&split).expect("invalid abi") + }); + } + }; + quote! { // Inline ABI declaration - pub static #abi_name: Lazy = Lazy::new(|| serde_json::from_str(#abi) - .expect("invalid abi")); + #abi_parse // Struct declaration #[derive(Clone)] diff --git a/ethers-contract/src/base.rs b/ethers-contract/src/base.rs index 7946a95d..b4348439 100644 --- a/ethers-contract/src/base.rs +++ b/ethers-contract/src/base.rs @@ -1,12 +1,27 @@ use crate::Contract; use ethers_core::{ - abi::{Abi, FunctionExt}, - types::{Address, Selector}, + abi::{ + Abi, Detokenize, Error, Event, Function, FunctionExt, InvalidOutputType, RawLog, Tokenize, + }, + types::{Address, Bytes, Selector, H256}, }; use ethers_providers::Middleware; +use rustc_hex::ToHex; use std::{collections::HashMap, fmt::Debug, hash::Hash, sync::Arc}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum AbiError { + /// Thrown when the ABI decoding fails + #[error(transparent)] + DecodingError(#[from] ethers_core::abi::Error), + + /// Thrown when detokenizing an argument + #[error(transparent)] + DetokenizationError(#[from] InvalidOutputType), +} /// A reduced form of `Contract` which just takes the `abi` and produces /// ABI encoded data for its functions. @@ -30,6 +45,68 @@ impl From for BaseContract { } impl BaseContract { + /// Returns the ABI encoded data for the provided function and arguments + /// + /// If the function exists multiple times and you want to use one of the overloaded + /// versions, consider using `encode_with_selector` + pub fn encode(&self, name: &str, args: T) -> Result { + let function = self.abi.function(name)?; + encode_fn(function, args) + } + + /// Returns the ABI encoded data for the provided function selector and arguments + pub fn encode_with_selector( + &self, + signature: Selector, + args: T, + ) -> Result { + let function = self.get_from_signature(signature)?; + encode_fn(function, args) + } + + /// Decodes the provided ABI encoded function arguments with the selected function name. + /// + /// If the function exists multiple times and you want to use one of the overloaded + /// versions, consider using `decode_with_selector` + pub fn decode( + &self, + name: &str, + bytes: impl AsRef<[u8]>, + ) -> Result { + let function = self.abi.function(name)?; + decode_fn(function, bytes, true) + } + + /// Decodes for a given event name, given the `log.topics` and + /// `log.data` fields from the transaction receipt + pub fn decode_event( + &self, + name: &str, + topics: Vec, + data: Bytes, + ) -> Result { + let event = self.abi.event(name)?; + decode_event(event, topics, data) + } + + /// Decodes the provided ABI encoded bytes with the selected function selector + pub fn decode_with_selector( + &self, + signature: Selector, + bytes: impl AsRef<[u8]>, + ) -> Result { + let function = self.get_from_signature(signature)?; + decode_fn(function, bytes, true) + } + + fn get_from_signature(&self, signature: Selector) -> Result<&Function, AbiError> { + Ok(self + .methods + .get(&signature) + .map(|(name, index)| &self.abi.functions[name][*index]) + .ok_or_else(|| Error::InvalidName(signature.to_hex::()))?) + } + /// Returns a reference to the contract's ABI pub fn abi(&self) -> &Abi { &self.abi @@ -51,6 +128,49 @@ impl AsRef for BaseContract { } } +pub(crate) fn decode_event( + event: &Event, + topics: Vec, + data: Bytes, +) -> Result { + let tokens = event + .parse_log(RawLog { + topics, + data: data.0, + })? + .params + .into_iter() + .map(|param| param.value) + .collect::>(); + Ok(D::from_tokens(tokens)?) +} + +// Helper for encoding arguments for a specific function +pub(crate) fn encode_fn(function: &Function, args: T) -> Result { + let tokens = args.into_tokens(); + Ok(function.encode_input(&tokens).map(Into::into)?) +} + +// Helper for decoding bytes from a specific function +pub(crate) fn decode_fn( + function: &Function, + bytes: impl AsRef<[u8]>, + is_input: bool, +) -> Result { + let mut bytes = bytes.as_ref(); + if bytes.starts_with(&function.selector()) { + bytes = &bytes[4..]; + } + + let tokens = if is_input { + function.decode_input(bytes.as_ref())? + } else { + function.decode_output(bytes.as_ref())? + }; + + Ok(D::from_tokens(tokens)?) +} + /// Utility function for creating a mapping between a unique signature and a /// name-index pair for accessing contract ABI items. fn create_mapping( @@ -72,3 +192,70 @@ where }) .collect() } + +#[cfg(test)] +mod tests { + use super::*; + use ethers_core::{abi::parse_abi, types::U256}; + use rustc_hex::FromHex; + + #[test] + fn can_parse_function_inputs() { + let abi = BaseContract::from(parse_abi(&[ + "function approve(address _spender, uint256 value) external view returns (bool, bool)" + ]).unwrap()); + + let spender = "7a250d5630b4cf539739df2c5dacb4c659f2488d" + .parse::
() + .unwrap(); + let amount = U256::MAX; + + let encoded = abi.encode("approve", (spender, amount)).unwrap(); + + assert_eq!(encoded.0.to_hex::(), "095ea7b30000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488dffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + + let (spender2, amount2): (Address, U256) = abi.decode("approve", encoded).unwrap(); + assert_eq!(spender, spender2); + assert_eq!(amount, amount2); + } + + #[test] + fn can_parse_events() { + let abi = BaseContract::from( + parse_abi(&[ + "event Approval(address indexed owner, address indexed spender, uint256 value)", + ]) + .unwrap(), + ); + + let topics = vec![ + "8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925", + "000000000000000000000000e4e60fdf9bf188fa57b7a5022230363d5bd56d08", + "0000000000000000000000007a250d5630b4cf539739df2c5dacb4c659f2488d", + ] + .into_iter() + .map(|hash| hash.parse::().unwrap()) + .collect::>(); + let data = Bytes::from( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + .from_hex::>() + .unwrap(), + ); + + let (owner, spender, value): (Address, Address, U256) = + abi.decode_event("Approval", topics, data).unwrap(); + assert_eq!(value, U256::MAX); + assert_eq!( + owner, + "e4e60fdf9bf188fa57b7a5022230363d5bd56d08" + .parse::
() + .unwrap() + ); + assert_eq!( + spender, + "7a250d5630b4cf539739df2c5dacb4c659f2488d" + .parse::
() + .unwrap() + ); + } +} diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index 7b684219..3b2a0cfd 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -1,5 +1,6 @@ +use super::base::{decode_fn, AbiError}; use ethers_core::{ - abi::{Detokenize, Error as AbiError, Function, InvalidOutputType}, + abi::{Detokenize, Function, InvalidOutputType}, types::{Address, BlockNumber, Bytes, TransactionRequest, TxHash, U256}, }; use ethers_providers::Middleware; @@ -13,7 +14,11 @@ use thiserror::Error as ThisError; pub enum ContractError { /// Thrown when the ABI decoding fails #[error(transparent)] - DecodingError(#[from] AbiError), + DecodingError(#[from] ethers_core::abi::Error), + + /// Thrown when the internal BaseContract errors + #[error(transparent)] + AbiError(#[from] AbiError), /// Thrown when detokenizing an argument #[error(transparent)] @@ -114,8 +119,8 @@ where .await .map_err(ContractError::MiddlewareError)?; - let tokens = self.function.decode_output(&bytes.0)?; - let data = D::from_tokens(tokens)?; + // decode output + let data = decode_fn(&self.function, &bytes, false)?; Ok(data) } diff --git a/ethers-contract/src/contract.rs b/ethers-contract/src/contract.rs index 14b5838d..d8077d7f 100644 --- a/ethers-contract/src/contract.rs +++ b/ethers-contract/src/contract.rs @@ -1,4 +1,8 @@ -use super::{base::BaseContract, call::ContractCall, event::Event}; +use super::{ + base::{encode_fn, AbiError, BaseContract}, + call::ContractCall, + event::Event, +}; use ethers_core::{ abi::{Abi, Detokenize, Error, EventExt, Function, Tokenize}, @@ -196,7 +200,7 @@ impl Contract { &self, name: &str, args: T, - ) -> Result, Error> { + ) -> Result, AbiError> { // get the function let function = self.base_contract.abi.function(name)?; self.method_func(function, args) @@ -208,7 +212,7 @@ impl Contract { &self, signature: Selector, args: T, - ) -> Result, Error> { + ) -> Result, AbiError> { let function = self .base_contract .methods @@ -222,16 +226,13 @@ impl Contract { &self, function: &Function, args: T, - ) -> Result, Error> { - let tokens = args.into_tokens(); - - // create the calldata - let data = function.encode_input(&tokens)?; + ) -> Result, AbiError> { + let data = encode_fn(function, args)?; // create the tx object let tx = TransactionRequest { to: Some(NameOrAddress::Address(self.address)), - data: Some(data.into()), + data: Some(data), ..Default::default() }; diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs index 401dc01b..f5c6432e 100644 --- a/ethers-contract/src/event.rs +++ b/ethers-contract/src/event.rs @@ -1,9 +1,9 @@ -use crate::ContractError; +use crate::{base::decode_event, ContractError}; use ethers_providers::Middleware; use ethers_core::{ - abi::{Detokenize, Event as AbiEvent, RawLog}, + abi::{Detokenize, Event as AbiEvent}, types::{BlockNumber, Filter, Log, TxHash, ValueOrArray, H256, U64}, }; @@ -122,20 +122,7 @@ where } fn parse_log(&self, log: Log) -> Result> { - // ethabi parses the unindexed and indexed logs together to a - // vector of tokens - let tokens = self - .event - .parse_log(RawLog { - topics: log.topics, - data: log.data.0, - })? - .params - .into_iter() - .map(|param| param.value) - .collect::>(); - // convert the tokens to the requested datatype - Ok(D::from_tokens(tokens)?) + Ok(decode_event(self.event, log.topics, log.data)?) } } diff --git a/ethers-core/src/abi/human_readable.rs b/ethers-core/src/abi/human_readable.rs new file mode 100644 index 00000000..02c890a6 --- /dev/null +++ b/ethers-core/src/abi/human_readable.rs @@ -0,0 +1,291 @@ +use super::{ + param_type::Reader, Abi, Event, EventParam, Function, Param, ParamType, StateMutability, +}; +use std::collections::HashMap; +use thiserror::Error; + +/// 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 line in input { + if line.contains("function") { + let function = parse_function(&line)?; + abi.functions + .entry(function.name.clone()) + .or_default() + .push(function); + } else if line.contains("event") { + let event = parse_event(&line)?; + abi.events + .entry(event.name.clone()) + .or_default() + .push(event); + } else if line.starts_with("struct") { + panic!("Got tuple"); + } else { + panic!("unknown sig") + } + } + + Ok(abi) +} + +fn parse_event(event: &str) -> Result { + let split: Vec<&str> = event.split("event ").collect(); + let split: Vec<&str> = split[1].split('(').collect(); + let name = split[0].trim_end(); + let rest = split[1]; + + let args = rest.replace(")", ""); + let anonymous = rest.contains("anonymous"); + + let inputs = if args.contains(',') { + let args: Vec<&str> = args.split(", ").collect(); + args.iter() + .map(|arg| parse_event_arg(arg)) + .collect::, _>>()? + } else { + vec![] + }; + + Ok(Event { + name: name.to_owned(), + anonymous, + inputs, + }) +} + +// Parses an event's argument as indexed if neded +fn parse_event_arg(param: &str) -> Result { + let tokens: Vec<&str> = param.split(' ').collect(); + let kind: ParamType = Reader::read(tokens[0])?; + let (name, indexed) = if tokens.len() == 2 { + (tokens[1], false) + } else { + (tokens[2], true) + }; + + Ok(EventParam { + name: name.to_owned(), + kind, + indexed, + }) +} + +fn parse_function(fn_string: &str) -> Result { + let fn_string = fn_string.to_owned(); + let delim = if fn_string.starts_with("function ") { + "function " + } else { + " " + }; + let split: Vec<&str> = fn_string.split(delim).collect(); + let split: Vec<&str> = split[1].split('(').collect(); + + // function name is the first char + let fn_name = split[0]; + + // internal args + let args: Vec<&str> = split[1].split(')').collect(); + let args: Vec<&str> = args[0].split(", ").collect(); + let inputs = args + .into_iter() + .filter(|x| !x.is_empty()) + .filter(|x| !x.contains("returns")) + .map(|x| parse_param(x)) + .collect::, _>>()?; + + // return value + let outputs: Vec = if split.len() > 2 { + let ret = split[2].strip_suffix(")").expect("no right paren"); + let ret: Vec<&str> = ret.split(", ").collect(); + + ret.into_iter() + // remove modifiers etc + .filter(|x| !x.is_empty()) + .map(|x| parse_param(x)) + .collect::, _>>()? + } else { + vec![] + }; + + Ok(Function { + name: fn_name.to_owned(), + inputs, + outputs, + // this doesn't really matter + state_mutability: StateMutability::Nonpayable, + }) +} + +// address x +fn parse_param(param: &str) -> Result { + let mut param = param + .split(' ') + .filter(|x| !x.contains("memory") || !x.contains("calldata")); + + let kind = param.next().ok_or(ParseError::Kind)?; + let kind: ParamType = Reader::read(kind).unwrap(); + + // strip memory/calldata from the name + // e.g. uint256[] memory x + let mut name = param.next().unwrap_or_default(); + if name == "memory" || name == "calldata" { + name = param.next().ok_or(ParseError::Kind)?; + } + + Ok(Param { + name: name.to_owned(), + kind, + }) +} + +#[derive(Error, Debug)] +pub enum ParseError { + #[error("expected data type")] + Kind, + + #[error(transparent)] + ParseError(#[from] super::Error), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_approve() { + let fn_str = "function approve(address _spender, uint256 value) external returns(bool)"; + let parsed = 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,); + assert_eq!(parsed.inputs[1].name, "value"); + assert_eq!(parsed.inputs[1].kind, ParamType::Uint(256),); + assert_eq!(parsed.outputs[0].name, ""); + assert_eq!(parsed.outputs[0].kind, ParamType::Bool); + } + + #[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(); + assert_eq!(parsed.name, "foo"); + assert_eq!(parsed.inputs[0].name, "x"); + assert_eq!( + parsed.inputs[0].kind, + ParamType::Array(Box::new(ParamType::Uint(32))) + ); + assert_eq!(parsed.outputs[0].name, ""); + assert_eq!(parsed.outputs[0].kind, ParamType::Address); + } + + #[test] + fn parses_function_empty() { + let fn_str = "function foo()"; + let parsed = parse_function(fn_str).unwrap(); + assert_eq!(parsed.name, "foo"); + assert!(parsed.inputs.is_empty()); + assert!(parsed.outputs.is_empty()); + } + + #[test] + fn parses_event() { + assert_eq!( + parse_event("event Foo (address indexed x, uint y, bytes32[] z)").unwrap(), + Event { + anonymous: false, + name: "Foo".to_owned(), + inputs: vec![ + EventParam { + name: "x".to_owned(), + kind: ParamType::Address, + indexed: true + }, + EventParam { + name: "y".to_owned(), + kind: ParamType::Uint(256), + indexed: false, + }, + EventParam { + name: "z".to_owned(), + kind: ParamType::Array(Box::new(ParamType::FixedBytes(32))), + indexed: false, + }, + ] + } + ); + } + + #[test] + fn parses_anonymous_event() { + assert_eq!( + parse_event("event Foo() anonymous").unwrap(), + Event { + anonymous: true, + name: "Foo".to_owned(), + inputs: vec![], + } + ); + } + + #[test] + fn parse_event_input() { + assert_eq!( + parse_event_arg("address indexed x").unwrap(), + EventParam { + name: "x".to_owned(), + kind: ParamType::Address, + indexed: true + } + ); + + assert_eq!( + parse_event_arg("address x").unwrap(), + EventParam { + name: "x".to_owned(), + kind: ParamType::Address, + indexed: false + } + ); + } + + #[test] + fn can_parse_functions() { + [ + "function foo(uint256[] memory x) external view returns (address)", + "function bar(uint256[] memory x) returns (address)", + "function bar(uint256[] memory x, uint32 y) returns (address, uint256)", + "function bar(uint256[] memory x)", + "function bar()", + ] + .iter() + .for_each(|x| { + parse_function(x).unwrap(); + }); + } + + #[test] + fn can_read_backslashes() { + parse(&[ + "\"function setValue(string)\"", + "\"function getValue() external view (string)\"", + ]) + .unwrap(); + } +} diff --git a/ethers-core/src/abi/mod.rs b/ethers-core/src/abi/mod.rs index 437717e5..ac49ef01 100644 --- a/ethers-core/src/abi/mod.rs +++ b/ethers-core/src/abi/mod.rs @@ -8,6 +8,9 @@ pub use ethabi::*; mod tokens; pub use tokens::{Detokenize, InvalidOutputType, Tokenizable, TokenizableItem, Tokenize}; +mod human_readable; +pub use human_readable::parse as parse_abi; + /// Extension trait for `ethabi::Function`. pub trait FunctionExt { /// Compute the method signature in the standard ABI format. This does not diff --git a/ethers/examples/contract.rs b/ethers/examples/contract.rs index d9ddb30c..d031b69a 100644 --- a/ethers/examples/contract.rs +++ b/ethers/examples/contract.rs @@ -6,9 +6,14 @@ use ethers::{ use std::{convert::TryFrom, sync::Arc, time::Duration}; // Generate the type-safe contract bindings by providing the ABI +// definition in human readable format abigen!( SimpleContract, - r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":false,"internalType":"string","name":"oldValue","type":"string"},{"indexed":false,"internalType":"string","name":"newValue","type":"string"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#, + r#"[ + function setValue(string) + function getValue() external view (string) + event ValueChanged(address indexed author, address indexed oldAuthor, string oldValue, string newValue) + ]"#, event_derives(serde::Deserialize, serde::Serialize) );