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
This commit is contained in:
parent
35e24ed412
commit
eb26915081
|
@ -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<Self> {
|
||||
// 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,
|
||||
|
|
|
@ -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<Abi> = Lazy::new(|| serde_json::from_str(#abi)
|
||||
.expect("invalid abi"));
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
pub static #abi_name: Lazy<Abi> = 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<Abi> = Lazy::new(|| serde_json::from_str(#abi)
|
||||
.expect("invalid abi"));
|
||||
#abi_parse
|
||||
|
||||
// Struct declaration
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -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<Abi> 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<T: Tokenize>(&self, name: &str, args: T) -> Result<Bytes, AbiError> {
|
||||
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<T: Tokenize>(
|
||||
&self,
|
||||
signature: Selector,
|
||||
args: T,
|
||||
) -> Result<Bytes, AbiError> {
|
||||
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<D: Detokenize>(
|
||||
&self,
|
||||
name: &str,
|
||||
bytes: impl AsRef<[u8]>,
|
||||
) -> Result<D, AbiError> {
|
||||
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<D: Detokenize>(
|
||||
&self,
|
||||
name: &str,
|
||||
topics: Vec<H256>,
|
||||
data: Bytes,
|
||||
) -> Result<D, AbiError> {
|
||||
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<D: Detokenize>(
|
||||
&self,
|
||||
signature: Selector,
|
||||
bytes: impl AsRef<[u8]>,
|
||||
) -> Result<D, AbiError> {
|
||||
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::<String>()))?)
|
||||
}
|
||||
|
||||
/// Returns a reference to the contract's ABI
|
||||
pub fn abi(&self) -> &Abi {
|
||||
&self.abi
|
||||
|
@ -51,6 +128,49 @@ impl AsRef<Abi> for BaseContract {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn decode_event<D: Detokenize>(
|
||||
event: &Event,
|
||||
topics: Vec<H256>,
|
||||
data: Bytes,
|
||||
) -> Result<D, AbiError> {
|
||||
let tokens = event
|
||||
.parse_log(RawLog {
|
||||
topics,
|
||||
data: data.0,
|
||||
})?
|
||||
.params
|
||||
.into_iter()
|
||||
.map(|param| param.value)
|
||||
.collect::<Vec<_>>();
|
||||
Ok(D::from_tokens(tokens)?)
|
||||
}
|
||||
|
||||
// Helper for encoding arguments for a specific function
|
||||
pub(crate) fn encode_fn<T: Tokenize>(function: &Function, args: T) -> Result<Bytes, AbiError> {
|
||||
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<D: Detokenize>(
|
||||
function: &Function,
|
||||
bytes: impl AsRef<[u8]>,
|
||||
is_input: bool,
|
||||
) -> Result<D, AbiError> {
|
||||
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<T, S, F>(
|
||||
|
@ -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::<Address>()
|
||||
.unwrap();
|
||||
let amount = U256::MAX;
|
||||
|
||||
let encoded = abi.encode("approve", (spender, amount)).unwrap();
|
||||
|
||||
assert_eq!(encoded.0.to_hex::<String>(), "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::<H256>().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let data = Bytes::from(
|
||||
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
|
||||
.from_hex::<Vec<u8>>()
|
||||
.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::<Address>()
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
spender,
|
||||
"7a250d5630b4cf539739df2c5dacb4c659f2488d"
|
||||
.parse::<Address>()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<M: Middleware> {
|
||||
/// 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)
|
||||
}
|
||||
|
|
|
@ -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<M: Middleware> Contract<M> {
|
|||
&self,
|
||||
name: &str,
|
||||
args: T,
|
||||
) -> Result<ContractCall<M, D>, Error> {
|
||||
) -> Result<ContractCall<M, D>, AbiError> {
|
||||
// get the function
|
||||
let function = self.base_contract.abi.function(name)?;
|
||||
self.method_func(function, args)
|
||||
|
@ -208,7 +212,7 @@ impl<M: Middleware> Contract<M> {
|
|||
&self,
|
||||
signature: Selector,
|
||||
args: T,
|
||||
) -> Result<ContractCall<M, D>, Error> {
|
||||
) -> Result<ContractCall<M, D>, AbiError> {
|
||||
let function = self
|
||||
.base_contract
|
||||
.methods
|
||||
|
@ -222,16 +226,13 @@ impl<M: Middleware> Contract<M> {
|
|||
&self,
|
||||
function: &Function,
|
||||
args: T,
|
||||
) -> Result<ContractCall<M, D>, Error> {
|
||||
let tokens = args.into_tokens();
|
||||
|
||||
// create the calldata
|
||||
let data = function.encode_input(&tokens)?;
|
||||
) -> Result<ContractCall<M, D>, 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()
|
||||
};
|
||||
|
||||
|
|
|
@ -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<D, ContractError<M>> {
|
||||
// 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::<Vec<_>>();
|
||||
// convert the tokens to the requested datatype
|
||||
Ok(D::from_tokens(tokens)?)
|
||||
Ok(decode_event(self.event, log.topics, log.data)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Abi, ParseError> {
|
||||
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<Event, ParseError> {
|
||||
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::<Result<Vec<EventParam>, _>>()?
|
||||
} 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<EventParam, ParseError> {
|
||||
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<Function, ParseError> {
|
||||
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::<Result<Vec<Param>, _>>()?;
|
||||
|
||||
// return value
|
||||
let outputs: Vec<Param> = 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::<Result<Vec<Param>, _>>()?
|
||||
} 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<Param, ParseError> {
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in New Issue