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::util;
|
||||||
use super::Abigen;
|
use super::Abigen;
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
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 inflector::Inflector;
|
||||||
use proc_macro2::{Ident, Literal, TokenStream};
|
use proc_macro2::{Ident, Literal, TokenStream};
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
@ -22,6 +25,9 @@ pub(crate) struct Context {
|
||||||
/// The parsed ABI.
|
/// The parsed ABI.
|
||||||
abi: Abi,
|
abi: Abi,
|
||||||
|
|
||||||
|
/// Was the ABI in human readable format?
|
||||||
|
human_readable: bool,
|
||||||
|
|
||||||
/// The contract name as an identifier.
|
/// The contract name as an identifier.
|
||||||
contract_name: Ident,
|
contract_name: Ident,
|
||||||
|
|
||||||
|
@ -92,13 +98,23 @@ impl Context {
|
||||||
fn from_abigen(args: Abigen) -> Result<Self> {
|
fn from_abigen(args: Abigen) -> Result<Self> {
|
||||||
// get the actual ABI string
|
// get the actual ABI string
|
||||||
let abi_str = args.abi_source.get().context("failed to get ABI JSON")?;
|
let abi_str = args.abi_source.get().context("failed to get ABI JSON")?;
|
||||||
|
|
||||||
// parse it
|
// parse it
|
||||||
let abi: Abi = serde_json::from_str(&abi_str)
|
let (abi, human_readable): (Abi, _) = if let Ok(abi) = serde_json::from_str(&abi_str) {
|
||||||
.with_context(|| format!("invalid artifact JSON '{}'", abi_str))
|
// normal abi format
|
||||||
.with_context(|| {
|
(abi, false)
|
||||||
format!("failed to parse artifact from source {:?}", args.abi_source,)
|
} 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);
|
let contract_name = util::ident(&args.contract_name);
|
||||||
|
|
||||||
|
@ -125,6 +141,7 @@ impl Context {
|
||||||
|
|
||||||
Ok(Context {
|
Ok(Context {
|
||||||
abi,
|
abi,
|
||||||
|
human_readable,
|
||||||
abi_str: Literal::string(&abi_str),
|
abi_str: Literal::string(&abi_str),
|
||||||
contract_name,
|
contract_name,
|
||||||
method_aliases,
|
method_aliases,
|
||||||
|
|
|
@ -15,7 +15,7 @@ pub(crate) fn imports(name: &str) -> TokenStream {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use ethers::{
|
use ethers::{
|
||||||
core::{
|
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
|
types::*, // import all the types so that we can codegen for everything
|
||||||
},
|
},
|
||||||
contract::{Contract, builders::{ContractCall, Event}, Lazy},
|
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 name = &cx.contract_name;
|
||||||
let abi = &cx.abi_str;
|
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! {
|
quote! {
|
||||||
// Inline ABI declaration
|
// Inline ABI declaration
|
||||||
pub static #abi_name: Lazy<Abi> = Lazy::new(|| serde_json::from_str(#abi)
|
#abi_parse
|
||||||
.expect("invalid abi"));
|
|
||||||
|
|
||||||
// Struct declaration
|
// Struct declaration
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
|
@ -1,12 +1,27 @@
|
||||||
use crate::Contract;
|
use crate::Contract;
|
||||||
|
|
||||||
use ethers_core::{
|
use ethers_core::{
|
||||||
abi::{Abi, FunctionExt},
|
abi::{
|
||||||
types::{Address, Selector},
|
Abi, Detokenize, Error, Event, Function, FunctionExt, InvalidOutputType, RawLog, Tokenize,
|
||||||
|
},
|
||||||
|
types::{Address, Bytes, Selector, H256},
|
||||||
};
|
};
|
||||||
use ethers_providers::Middleware;
|
use ethers_providers::Middleware;
|
||||||
|
|
||||||
|
use rustc_hex::ToHex;
|
||||||
use std::{collections::HashMap, fmt::Debug, hash::Hash, sync::Arc};
|
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
|
/// A reduced form of `Contract` which just takes the `abi` and produces
|
||||||
/// ABI encoded data for its functions.
|
/// ABI encoded data for its functions.
|
||||||
|
@ -30,6 +45,68 @@ impl From<Abi> for BaseContract {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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
|
/// Returns a reference to the contract's ABI
|
||||||
pub fn abi(&self) -> &Abi {
|
pub fn abi(&self) -> &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
|
/// Utility function for creating a mapping between a unique signature and a
|
||||||
/// name-index pair for accessing contract ABI items.
|
/// name-index pair for accessing contract ABI items.
|
||||||
fn create_mapping<T, S, F>(
|
fn create_mapping<T, S, F>(
|
||||||
|
@ -72,3 +192,70 @@ where
|
||||||
})
|
})
|
||||||
.collect()
|
.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::{
|
use ethers_core::{
|
||||||
abi::{Detokenize, Error as AbiError, Function, InvalidOutputType},
|
abi::{Detokenize, Function, InvalidOutputType},
|
||||||
types::{Address, BlockNumber, Bytes, TransactionRequest, TxHash, U256},
|
types::{Address, BlockNumber, Bytes, TransactionRequest, TxHash, U256},
|
||||||
};
|
};
|
||||||
use ethers_providers::Middleware;
|
use ethers_providers::Middleware;
|
||||||
|
@ -13,7 +14,11 @@ use thiserror::Error as ThisError;
|
||||||
pub enum ContractError<M: Middleware> {
|
pub enum ContractError<M: Middleware> {
|
||||||
/// Thrown when the ABI decoding fails
|
/// Thrown when the ABI decoding fails
|
||||||
#[error(transparent)]
|
#[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
|
/// Thrown when detokenizing an argument
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
@ -114,8 +119,8 @@ where
|
||||||
.await
|
.await
|
||||||
.map_err(ContractError::MiddlewareError)?;
|
.map_err(ContractError::MiddlewareError)?;
|
||||||
|
|
||||||
let tokens = self.function.decode_output(&bytes.0)?;
|
// decode output
|
||||||
let data = D::from_tokens(tokens)?;
|
let data = decode_fn(&self.function, &bytes, false)?;
|
||||||
|
|
||||||
Ok(data)
|
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::{
|
use ethers_core::{
|
||||||
abi::{Abi, Detokenize, Error, EventExt, Function, Tokenize},
|
abi::{Abi, Detokenize, Error, EventExt, Function, Tokenize},
|
||||||
|
@ -196,7 +200,7 @@ impl<M: Middleware> Contract<M> {
|
||||||
&self,
|
&self,
|
||||||
name: &str,
|
name: &str,
|
||||||
args: T,
|
args: T,
|
||||||
) -> Result<ContractCall<M, D>, Error> {
|
) -> Result<ContractCall<M, D>, AbiError> {
|
||||||
// get the function
|
// get the function
|
||||||
let function = self.base_contract.abi.function(name)?;
|
let function = self.base_contract.abi.function(name)?;
|
||||||
self.method_func(function, args)
|
self.method_func(function, args)
|
||||||
|
@ -208,7 +212,7 @@ impl<M: Middleware> Contract<M> {
|
||||||
&self,
|
&self,
|
||||||
signature: Selector,
|
signature: Selector,
|
||||||
args: T,
|
args: T,
|
||||||
) -> Result<ContractCall<M, D>, Error> {
|
) -> Result<ContractCall<M, D>, AbiError> {
|
||||||
let function = self
|
let function = self
|
||||||
.base_contract
|
.base_contract
|
||||||
.methods
|
.methods
|
||||||
|
@ -222,16 +226,13 @@ impl<M: Middleware> Contract<M> {
|
||||||
&self,
|
&self,
|
||||||
function: &Function,
|
function: &Function,
|
||||||
args: T,
|
args: T,
|
||||||
) -> Result<ContractCall<M, D>, Error> {
|
) -> Result<ContractCall<M, D>, AbiError> {
|
||||||
let tokens = args.into_tokens();
|
let data = encode_fn(function, args)?;
|
||||||
|
|
||||||
// create the calldata
|
|
||||||
let data = function.encode_input(&tokens)?;
|
|
||||||
|
|
||||||
// create the tx object
|
// create the tx object
|
||||||
let tx = TransactionRequest {
|
let tx = TransactionRequest {
|
||||||
to: Some(NameOrAddress::Address(self.address)),
|
to: Some(NameOrAddress::Address(self.address)),
|
||||||
data: Some(data.into()),
|
data: Some(data),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use crate::ContractError;
|
use crate::{base::decode_event, ContractError};
|
||||||
|
|
||||||
use ethers_providers::Middleware;
|
use ethers_providers::Middleware;
|
||||||
|
|
||||||
use ethers_core::{
|
use ethers_core::{
|
||||||
abi::{Detokenize, Event as AbiEvent, RawLog},
|
abi::{Detokenize, Event as AbiEvent},
|
||||||
types::{BlockNumber, Filter, Log, TxHash, ValueOrArray, H256, U64},
|
types::{BlockNumber, Filter, Log, TxHash, ValueOrArray, H256, U64},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -122,20 +122,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_log(&self, log: Log) -> Result<D, ContractError<M>> {
|
fn parse_log(&self, log: Log) -> Result<D, ContractError<M>> {
|
||||||
// ethabi parses the unindexed and indexed logs together to a
|
Ok(decode_event(self.event, log.topics, log.data)?)
|
||||||
// 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)?)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
mod tokens;
|
||||||
pub use tokens::{Detokenize, InvalidOutputType, Tokenizable, TokenizableItem, Tokenize};
|
pub use tokens::{Detokenize, InvalidOutputType, Tokenizable, TokenizableItem, Tokenize};
|
||||||
|
|
||||||
|
mod human_readable;
|
||||||
|
pub use human_readable::parse as parse_abi;
|
||||||
|
|
||||||
/// Extension trait for `ethabi::Function`.
|
/// Extension trait for `ethabi::Function`.
|
||||||
pub trait FunctionExt {
|
pub trait FunctionExt {
|
||||||
/// Compute the method signature in the standard ABI format. This does not
|
/// 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};
|
use std::{convert::TryFrom, sync::Arc, time::Duration};
|
||||||
|
|
||||||
// Generate the type-safe contract bindings by providing the ABI
|
// Generate the type-safe contract bindings by providing the ABI
|
||||||
|
// definition in human readable format
|
||||||
abigen!(
|
abigen!(
|
||||||
SimpleContract,
|
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)
|
event_derives(serde::Deserialize, serde::Serialize)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue