Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Commit

Permalink
feat: allow encoding/decoding function data (#90)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
gakonst authored Oct 29, 2020
1 parent 35e24ed commit eb26915
Show file tree
Hide file tree
Showing 9 changed files with 557 additions and 42 deletions.
31 changes: 24 additions & 7 deletions ethers-contract/ethers-contract-abigen/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,

Expand Down Expand Up @@ -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);

Expand All @@ -125,6 +141,7 @@ impl Context {

Ok(Context {
abi,
human_readable,
abi_str: Literal::string(&abi_str),
contract_name,
method_aliases,
Expand Down
25 changes: 22 additions & 3 deletions ethers-contract/ethers-contract-abigen/src/contract/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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)]
Expand Down
191 changes: 189 additions & 2 deletions ethers-contract/src/base.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand All @@ -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>(
Expand All @@ -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()
);
}
}
13 changes: 9 additions & 4 deletions ethers-contract/src/call.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)]
Expand Down Expand Up @@ -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)
}
Expand Down
Loading

0 comments on commit eb26915

Please sign in to comment.