Expose contract revert errors in the ContractError struct (#2172)

* feature: spelunk for revert errors

* feature: bubble up revert to contract error

* feature: bubble up reverts to multicall

* fix: correctly remove signature when deserializing EthErrors

* chore: remove redundant test

* chore: clippy

* fix: allow empty revert string

* docs: add all missing rustdoc for ethers-contract

* chore: rustfmt

* chore: Changelog

* fix: danipope test comment
This commit is contained in:
James Prestwich 2023-02-22 14:52:25 -08:00 committed by GitHub
parent ee5e3e52c0
commit 2090bf560e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 492 additions and 231 deletions

View File

@ -302,6 +302,11 @@
### Unreleased
- (Breaking) Add `Revert` to `ContractError`. Add `impl EthError for String`.
Modify existing `ContractError` variants to prevent accidental improper
usage. Change `MulticallError` to use `ContractError::Revert`. Add
convenience methods to decode errors from reverts.
[#2172](https://github.com/gakonst/ethers-rs/pull/2172)
- (Breaking) Improve Multicall result handling
[#2164](https://github.com/gakonst/ethers-rs/pull/2105)
- (Breaking) Make `Event` objects generic over borrow & remove lifetime

View File

@ -159,6 +159,8 @@ impl BaseContract {
decode_function_data(function, bytes, true)
}
/// Decode the provided ABI encoded bytes as the output of the provided
/// function selector
pub fn decode_output_with_selector<D: Detokenize, T: AsRef<[u8]>>(
&self,
signature: Selector,

View File

@ -1,5 +1,7 @@
#![allow(clippy::return_self_not_must_use)]
use crate::EthError;
use super::base::{decode_function_data, AbiError};
use ethers_core::{
abi::{AbiDecode, AbiEncode, Detokenize, Function, InvalidOutputType, Tokenizable},
@ -11,7 +13,7 @@ use ethers_core::{
};
use ethers_providers::{
call_raw::{CallBuilder, RawCall},
Middleware, PendingTransaction, ProviderError,
JsonRpcError, Middleware, MiddlewareError, PendingTransaction, ProviderError,
};
use std::{
@ -54,12 +56,22 @@ pub enum ContractError<M: Middleware> {
DetokenizationError(#[from] InvalidOutputType),
/// Thrown when a middleware call fails
#[error("{0}")]
MiddlewareError(M::Error),
#[error("{e}")]
MiddlewareError {
/// The underlying error
e: M::Error,
},
/// Thrown when a provider call fails
#[error("{0}")]
ProviderError(ProviderError),
#[error("{e}")]
ProviderError {
/// The underlying error
e: ProviderError,
},
/// Contract reverted
#[error("Contract call reverted with data: {0}")]
Revert(Bytes),
/// Thrown during deployment if a constructor argument was passed in the `deploy`
/// call but a constructor was not present in the ABI
@ -72,6 +84,83 @@ pub enum ContractError<M: Middleware> {
ContractNotDeployed,
}
impl<M: Middleware> ContractError<M> {
/// If this `ContractError` is a revert, this method will retrieve a
/// reference to the underlying revert data. This ABI-encoded data could be
/// a String, or a custom Solidity error type.
///
/// ## Returns
///
/// `None` if the error is not a revert
/// `Some(data)` with the revert data, if the error is a revert
///
/// ## Note
///
/// To skip this step, consider using [`ContractError::decode_revert`]
pub fn as_revert(&self) -> Option<&Bytes> {
match self {
ContractError::Revert(data) => Some(data),
_ => None,
}
}
/// True if the error is a revert, false otherwise
pub fn is_revert(&self) -> bool {
matches!(self, ContractError::Revert(_))
}
/// Decode revert data into an [`EthError`] type. Returns `None` if
/// decoding fails, or if this is not a revert
pub fn decode_revert<Err: EthError>(&self) -> Option<Err> {
self.as_revert().and_then(|data| Err::decode_with_selector(data))
}
/// Convert a [`MiddlewareError`] to a `ContractError`
pub fn from_middleware_error(e: M::Error) -> Self {
if let Some(data) = e.as_error_response().and_then(JsonRpcError::as_revert_data) {
ContractError::Revert(data)
} else {
ContractError::MiddlewareError { e }
}
}
/// Convert a `ContractError` to a [`MiddlewareError`] if possible.
pub fn as_middleware_error(&self) -> Option<&M::Error> {
match self {
ContractError::MiddlewareError { e } => Some(e),
_ => None,
}
}
/// True if the error is a middleware error
pub fn is_middleware_error(&self) -> bool {
matches!(self, ContractError::MiddlewareError { .. })
}
/// Convert a `ContractError` to a [`ProviderError`] if possible.
pub fn as_provider_error(&self) -> Option<&ProviderError> {
match self {
ContractError::ProviderError { e } => Some(e),
_ => None,
}
}
/// True if the error is a provider error
pub fn is_provider_error(&self) -> bool {
matches!(self, ContractError::ProviderError { .. })
}
}
impl<M: Middleware> From<ProviderError> for ContractError<M> {
fn from(e: ProviderError) -> Self {
if let Some(data) = e.as_error_response().and_then(JsonRpcError::as_revert_data) {
ContractError::Revert(data)
} else {
ContractError::ProviderError { e }
}
}
}
/// `ContractCall` is a [`FunctionCall`] object with an [`std::sync::Arc`] middleware.
/// This type alias exists to preserve backwards compatibility with
/// less-abstract Contracts.
@ -177,7 +266,7 @@ where
.borrow()
.estimate_gas(&self.tx, self.block)
.await
.map_err(ContractError::MiddlewareError)
.map_err(ContractError::from_middleware_error)
}
/// Queries the blockchain via an `eth_call` for the provided transaction.
@ -190,9 +279,12 @@ where
///
/// Note: this function _does not_ send a transaction from your account
pub async fn call(&self) -> Result<D, ContractError<M>> {
let client: &M = self.client.borrow();
let bytes =
client.call(&self.tx, self.block).await.map_err(ContractError::MiddlewareError)?;
let bytes = self
.client
.borrow()
.call(&self.tx, self.block)
.await
.map_err(ContractError::from_middleware_error)?;
// decode output
let data = decode_function_data(&self.function, &bytes, false)?;
@ -211,7 +303,7 @@ where
) -> impl RawCall<'_> + Future<Output = Result<D, ContractError<M>>> + Debug {
let call = self.call_raw_bytes();
call.map(move |res: Result<Bytes, ProviderError>| {
let bytes = res.map_err(ContractError::ProviderError)?;
let bytes = res?;
decode_function_data(&self.function, &bytes, false).map_err(From::from)
})
}
@ -237,7 +329,7 @@ where
.borrow()
.send_transaction(self.tx.clone(), self.block)
.await
.map_err(ContractError::MiddlewareError)
.map_err(ContractError::from_middleware_error)
}
}

View File

@ -1,12 +1,26 @@
use ethers_core::{
abi::{AbiDecode, AbiEncode, Tokenizable},
types::Selector,
types::{Bytes, Selector},
utils::id,
};
use ethers_providers::JsonRpcError;
use std::borrow::Cow;
/// A helper trait for types that represents a custom error type
pub trait EthError: Tokenizable + AbiDecode + AbiEncode + Send + Sync {
/// Attempt to decode from a [`JsonRpcError`] by extracting revert data
///
/// Fails if the error is not a revert, or decoding fails
fn from_rpc_response(response: &JsonRpcError) -> Option<Self> {
Self::decode_with_selector(&response.as_revert_data()?)
}
/// Decode the error from EVM revert data including an Error selector
fn decode_with_selector(data: &Bytes) -> Option<Self> {
// This will return none if selector mismatch.
<Self as AbiDecode>::decode(data.strip_prefix(&Self::selector())?).ok()
}
/// The name of the error
fn error_name() -> Cow<'static, str>;
@ -18,3 +32,30 @@ pub trait EthError: Tokenizable + AbiDecode + AbiEncode + Send + Sync {
id(Self::abi_signature())
}
}
impl EthError for String {
fn error_name() -> Cow<'static, str> {
Cow::Borrowed("Error")
}
fn abi_signature() -> Cow<'static, str> {
Cow::Borrowed("Error(string)")
}
}
#[cfg(test)]
mod test {
use ethers_core::types::Bytes;
use super::EthError;
#[test]
fn string_error() {
let multicall_revert_string: Bytes = "0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000174d756c746963616c6c333a2063616c6c206661696c6564000000000000000000".parse().unwrap();
assert_eq!(String::selector().as_slice(), &multicall_revert_string[0..4]);
assert_eq!(
String::decode_with_selector(&multicall_revert_string).unwrap().as_str(),
"Multicall3: call failed"
);
}
}

View File

@ -192,7 +192,7 @@ where
.borrow()
.watch(&self.filter)
.await
.map_err(ContractError::MiddlewareError)?;
.map_err(ContractError::from_middleware_error)?;
Ok(EventStream::new(filter.id, filter, Box::new(move |log| Ok(parse_log(log)?))))
}
@ -209,7 +209,7 @@ where
.borrow()
.watch(&self.filter)
.await
.map_err(ContractError::MiddlewareError)?;
.map_err(ContractError::from_middleware_error)?;
Ok(EventStream::new(
filter.id,
filter,
@ -243,10 +243,11 @@ where
.borrow()
.subscribe_logs(&self.filter)
.await
.map_err(ContractError::MiddlewareError)?;
.map_err(ContractError::from_middleware_error)?;
Ok(EventStream::new(filter.id, filter, Box::new(move |log| Ok(parse_log(log)?))))
}
/// As [`Self::subscribe`], but includes event metadata
pub async fn subscribe_with_meta(
&self,
) -> Result<
@ -259,7 +260,7 @@ where
.borrow()
.subscribe_logs(&self.filter)
.await
.map_err(ContractError::MiddlewareError)?;
.map_err(ContractError::from_middleware_error)?;
Ok(EventStream::new(
filter.id,
filter,
@ -285,7 +286,7 @@ where
.borrow()
.get_logs(&self.filter)
.await
.map_err(ContractError::MiddlewareError)?;
.map_err(ContractError::from_middleware_error)?;
let events = logs
.into_iter()
.map(|log| Ok(parse_log(log)?))
@ -301,7 +302,7 @@ where
.borrow()
.get_logs(&self.filter)
.await
.map_err(ContractError::MiddlewareError)?;
.map_err(ContractError::from_middleware_error)?;
let events = logs
.into_iter()
.map(|log| {

View File

@ -79,6 +79,7 @@ where
self
}
/// Sets the block at which RPC requests are made
pub fn block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
self.deployer.block = block.into();
self
@ -222,6 +223,7 @@ where
self
}
/// Set the block at which requests are made
pub fn block<T: Into<BlockNumber>>(mut self, block: T) -> Self {
self.block = block.into();
self
@ -247,7 +249,7 @@ where
.borrow()
.call(&self.tx, Some(self.block.into()))
.await
.map_err(ContractError::MiddlewareError)?;
.map_err(ContractError::from_middleware_error)?;
// TODO: It would be nice to handle reverts in a structured way.
Ok(())
@ -282,7 +284,7 @@ where
.borrow()
.send_transaction(self.tx, Some(self.block.into()))
.await
.map_err(ContractError::MiddlewareError)?;
.map_err(ContractError::from_middleware_error)?;
// TODO: Should this be calculated "optimistically" by address/nonce?
let receipt = pending_tx
@ -382,6 +384,8 @@ where
Self { client, abi, bytecode, _m: PhantomData }
}
/// Create a deployment tx using the provided tokens as constructor
/// arguments
pub fn deploy_tokens(self, params: Vec<Token>) -> Result<Deployer<B, M>, ContractError<M>>
where
B: Clone,

View File

@ -1,6 +1,7 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc = include_str!("../README.md")]
#![deny(unsafe_code)]
#![warn(missing_docs)]
mod contract;
pub use contract::{Contract, ContractInstance};
@ -31,8 +32,10 @@ mod multicall;
#[cfg(any(test, feature = "abigen"))]
#[cfg_attr(docsrs, doc(cfg(feature = "abigen")))]
pub use multicall::{
contract as multicall_contract, Call, Multicall, MulticallContract, MulticallError,
MulticallVersion, MULTICALL_ADDRESS, MULTICALL_SUPPORTED_CHAIN_IDS,
constants::{MULTICALL_ADDRESS, MULTICALL_SUPPORTED_CHAIN_IDS},
contract as multicall_contract,
error::MulticallError,
Call, Multicall, MulticallContract, MulticallVersion,
};
/// This module exposes low lever builder structures which are only consumed by the

View File

@ -0,0 +1,74 @@
use ethers_core::types::{Chain, H160};
/// The Multicall3 contract address that is deployed in [`MULTICALL_SUPPORTED_CHAIN_IDS`]:
/// [`0xcA11bde05977b3631167028862bE2a173976CA11`](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11)
pub const MULTICALL_ADDRESS: H160 = H160([
0xca, 0x11, 0xbd, 0xe0, 0x59, 0x77, 0xb3, 0x63, 0x11, 0x67, 0x02, 0x88, 0x62, 0xbe, 0x2a, 0x17,
0x39, 0x76, 0xca, 0x11,
]);
/// The chain IDs that [`MULTICALL_ADDRESS`] has been deployed to.
///
/// Taken from: <https://github.com/mds1/multicall#multicall3-contract-addresses>
pub const MULTICALL_SUPPORTED_CHAIN_IDS: &[u64] = {
use Chain::*;
&[
Mainnet as u64, // Mainnet
Kovan as u64, // Kovan
Rinkeby as u64, // Rinkeby
Goerli as u64, // Görli
Ropsten as u64, // Ropsten
Sepolia as u64, // Sepolia
Optimism as u64, // Optimism
OptimismKovan as u64, // Optimism Kovan
OptimismGoerli as u64, // Optimism Görli
Arbitrum as u64, // Arbitrum
ArbitrumNova as u64, // Arbitrum Nova
ArbitrumGoerli as u64, // Arbitrum Görli
ArbitrumTestnet as u64, // Arbitrum Rinkeby
Polygon as u64, // Polygon
PolygonMumbai as u64, // Polygon Mumbai
XDai as u64, // Gnosis Chain
Avalanche as u64, // Avalanche
AvalancheFuji as u64, // Avalanche Fuji
FantomTestnet as u64, // Fantom Testnet
Fantom as u64, // Fantom Opera
BinanceSmartChain as u64, // BNB Smart Chain
BinanceSmartChainTestnet as u64, // BNB Smart Chain Testnet
Moonbeam as u64, // Moonbeam
Moonriver as u64, // Moonriver
Moonbase as u64, // Moonbase
1666600000, // Harmony0
1666600001, // Harmony1
1666600002, // Harmony2
1666600003, // Harmony3
Cronos as u64, // Cronos
122, // Fuse
14, // Flare Mainnet
19, // Songbird Canary Network
16, // Coston Testnet
114, // Coston2 Testnet
288, // Boba
Aurora as u64, // Aurora
592, // Astar
66, // OKC
128, // Heco Chain
1088, // Metis
Rsk as u64, // Rsk
31, // Rsk Testnet
Evmos as u64, // Evmos
EvmosTestnet as u64, // Evmos Testnet
Oasis as u64, // Oasis
42261, // Oasis Emerald ParaTime Testnet
42262, // Oasis Emerald ParaTime
Celo as u64, // Celo
CeloAlfajores as u64, // Celo Alfajores Testnet
71402, // Godwoken
71401, // Godwoken Testnet
8217, // Klaytn
2001, // Milkomeda
321, // KCC
106, // Velas
40, // Telos
]
};

View File

@ -0,0 +1,4 @@
#![allow(missing_docs)]
use ethers_contract_derive::abigen;
abigen!(Multicall3, "src/multicall/multicall_abi.json");

View File

@ -0,0 +1,98 @@
use ethers_core::{
abi::{self, InvalidOutputType},
types::Bytes,
};
use ethers_providers::{Middleware, ProviderError};
use crate::{ContractError, EthError};
/// Errors using the [`crate::Multicall`] system
#[derive(Debug, thiserror::Error)]
pub enum MulticallError<M: Middleware> {
/// Contract call returned an error
#[error(transparent)]
ContractError(#[from] ContractError<M>),
/// Unsupported chain
#[error("Chain ID {0} is currently not supported by Multicall. Provide an address instead.")]
InvalidChainId(u64),
/// Contract call reverted when not allowed
#[error("Illegal revert: Multicall2 call reverted when it wasn't allowed to.")]
IllegalRevert,
}
impl<M: Middleware> From<abi::Error> for MulticallError<M> {
fn from(value: abi::Error) -> Self {
Self::ContractError(ContractError::DecodingError(value))
}
}
impl<M: Middleware> From<InvalidOutputType> for MulticallError<M> {
fn from(value: InvalidOutputType) -> Self {
Self::ContractError(ContractError::DetokenizationError(value))
}
}
impl<M: Middleware> MulticallError<M> {
/// Convert a `MulticallError` to a the underlying error if possible.
pub fn as_contract_error(&self) -> Option<&ContractError<M>> {
match self {
MulticallError::ContractError(e) => Some(e),
_ => None,
}
}
/// True if the underlying error is a [`ContractError`]
pub fn is_contract_error(&self) -> bool {
matches!(self, MulticallError::ContractError(_))
}
/// Convert a `MulticallError` to a the underlying error if possible.
pub fn as_middleware_error(&self) -> Option<&M::Error> {
self.as_contract_error().and_then(ContractError::as_middleware_error)
}
/// True if the underlying error is a MiddlewareError
pub fn is_middleware_error(&self) -> bool {
self.as_contract_error().map(ContractError::is_middleware_error).unwrap_or_default()
}
/// Convert a `MulticallError` to a [`ProviderError`] if possible.
pub fn as_provider_error(&self) -> Option<&ProviderError> {
self.as_contract_error().and_then(ContractError::as_provider_error)
}
/// True if the error is a provider error
pub fn is_provider_error(&self) -> bool {
self.as_contract_error().map(ContractError::is_provider_error).unwrap_or_default()
}
/// If this `MulticallError` is a revert, this method will retrieve a
/// reference to the underlying revert data. This ABI-encoded data could be
/// a String, or a custom Solidity error type.
///
/// ## Returns
///
/// `None` if the error is not a revert
/// `Some(data)` with the revert data, if the error is a revert
///
/// ## Note
///
/// To skip this step, consider using [`MulticallError::decode_revert`]
pub fn as_revert(&self) -> Option<&Bytes> {
self.as_contract_error().and_then(ContractError::as_revert)
}
/// True if the error is a revert, false otherwise
pub fn is_revert(&self) -> bool {
self.as_contract_error().map(ContractError::is_revert).unwrap_or_default()
}
/// Decode revert data into an [`EthError`] type. Returns `None` if
/// decoding fails, or if this is not a revert
pub fn decode_revert<Err: EthError>(&self) -> Option<Err> {
self.as_revert().and_then(|data| Err::decode_with_selector(data))
}
}

View File

@ -1,151 +1,28 @@
use crate::call::{ContractCall, ContractError};
use ethers_core::{
abi::{AbiDecode, Detokenize, Function, InvalidOutputType, Token, Tokenizable},
abi::{Detokenize, Function, Token, Tokenizable},
types::{
transaction::eip2718::TypedTransaction, Address, BlockNumber, Bytes, Chain, NameOrAddress,
H160, U256,
transaction::eip2718::TypedTransaction, Address, BlockNumber, Bytes, NameOrAddress, U256,
},
};
use ethers_providers::{Middleware, PendingTransaction};
use std::{convert::TryFrom, fmt, result::Result as StdResult, sync::Arc};
/// The Multicall contract bindings. Auto-generated with `abigen`.
pub mod contract {
ethers_contract_derive::abigen!(Multicall3, "src/multicall/multicall_abi.json");
}
pub mod contract;
pub use contract::Multicall3 as MulticallContract;
use contract::{
Call as Multicall1Call, Call3 as Multicall3Call, Call3Value as Multicall3CallValue,
Result as MulticallResult,
};
/// The Multicall3 contract address that is deployed in [`MULTICALL_SUPPORTED_CHAIN_IDS`]:
/// [`0xcA11bde05977b3631167028862bE2a173976CA11`](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11)
pub const MULTICALL_ADDRESS: Address = H160([
0xca, 0x11, 0xbd, 0xe0, 0x59, 0x77, 0xb3, 0x63, 0x11, 0x67, 0x02, 0x88, 0x62, 0xbe, 0x2a, 0x17,
0x39, 0x76, 0xca, 0x11,
]);
/// The chain IDs that [`MULTICALL_ADDRESS`] has been deployed to.
///
/// Taken from: <https://github.com/mds1/multicall#multicall3-contract-addresses>
pub const MULTICALL_SUPPORTED_CHAIN_IDS: &[u64] = {
use Chain::*;
&[
Mainnet as u64, // Mainnet
Kovan as u64, // Kovan
Rinkeby as u64, // Rinkeby
Goerli as u64, // Görli
Ropsten as u64, // Ropsten
Sepolia as u64, // Sepolia
Optimism as u64, // Optimism
OptimismKovan as u64, // Optimism Kovan
OptimismGoerli as u64, // Optimism Görli
Arbitrum as u64, // Arbitrum
ArbitrumNova as u64, // Arbitrum Nova
ArbitrumGoerli as u64, // Arbitrum Görli
ArbitrumTestnet as u64, // Arbitrum Rinkeby
Polygon as u64, // Polygon
PolygonMumbai as u64, // Polygon Mumbai
XDai as u64, // Gnosis Chain
Avalanche as u64, // Avalanche
AvalancheFuji as u64, // Avalanche Fuji
FantomTestnet as u64, // Fantom Testnet
Fantom as u64, // Fantom Opera
BinanceSmartChain as u64, // BNB Smart Chain
BinanceSmartChainTestnet as u64, // BNB Smart Chain Testnet
Moonbeam as u64, // Moonbeam
Moonriver as u64, // Moonriver
Moonbase as u64, // Moonbase
1666600000, // Harmony0
1666600001, // Harmony1
1666600002, // Harmony2
1666600003, // Harmony3
Cronos as u64, // Cronos
122, // Fuse
14, // Flare Mainnet
19, // Songbird Canary Network
16, // Coston Testnet
114, // Coston2 Testnet
288, // Boba
Aurora as u64, // Aurora
592, // Astar
66, // OKC
128, // Heco Chain
1088, // Metis
Rsk as u64, // Rsk
31, // Rsk Testnet
Evmos as u64, // Evmos
EvmosTestnet as u64, // Evmos Testnet
Oasis as u64, // Oasis
42261, // Oasis Emerald ParaTime Testnet
42262, // Oasis Emerald ParaTime
Celo as u64, // Celo
CeloAlfajores as u64, // Celo Alfajores Testnet
71402, // Godwoken
71401, // Godwoken Testnet
8217, // Klaytn
2001, // Milkomeda
321, // KCC
106, // Velas
40, // Telos
]
};
pub mod constants;
/// Type alias for `Result<T, MulticallError<M>>`
pub type Result<T, M> = StdResult<T, MulticallError<M>>;
pub type Result<T, M> = StdResult<T, error::MulticallError<M>>;
#[derive(Debug, thiserror::Error)]
pub enum MulticallError<M: Middleware> {
#[error(transparent)]
ContractError(#[from] ContractError<M>),
#[error("Chain ID {0} is currently not supported by Multicall. Provide an address instead.")]
InvalidChainId(u64),
#[error("Illegal revert: Multicall2 call reverted when it wasn't allowed to.")]
IllegalRevert,
#[error("Call reverted with data: \"{}\"", decode_error(_0))]
CallReverted(Bytes),
}
impl<M: Middleware> From<ethers_core::abi::Error> for MulticallError<M> {
fn from(value: ethers_core::abi::Error) -> Self {
Self::ContractError(ContractError::DecodingError(value))
}
}
impl<M: Middleware> From<InvalidOutputType> for MulticallError<M> {
fn from(value: InvalidOutputType) -> Self {
Self::ContractError(ContractError::DetokenizationError(value))
}
}
impl<M: Middleware> MulticallError<M> {
pub fn into_bytes(self) -> Result<Bytes, M> {
match self {
Self::CallReverted(bytes) => Ok(bytes),
e => Err(e),
}
}
/// Returns the bytes that the call reverted with.
pub fn get_bytes(&self) -> Option<&Bytes> {
match self {
Self::CallReverted(bytes) => Some(bytes),
_ => None,
}
}
/// Formats the bytes that the call reverted with.
pub fn format_bytes(&self) -> Option<String> {
match self {
Self::CallReverted(bytes) => Some(decode_error(bytes)),
_ => None,
}
}
}
/// MultiCall error type
pub mod error;
/// Helper struct for managing calls to be made to the `function` in smart contract `target`
/// with `data`.
@ -171,8 +48,11 @@ pub struct Call {
#[repr(u8)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum MulticallVersion {
/// V1
Multicall = 1,
/// V2
Multicall2 = 2,
/// V3
#[default]
Multicall3 = 3,
}
@ -196,16 +76,19 @@ impl TryFrom<u8> for MulticallVersion {
}
impl MulticallVersion {
/// True if call is v1
#[inline]
pub fn is_v1(&self) -> bool {
matches!(self, Self::Multicall)
}
/// True if call is v2
#[inline]
pub fn is_v2(&self) -> bool {
matches!(self, Self::Multicall2)
}
/// True if call is v3
#[inline]
pub fn is_v3(&self) -> bool {
matches!(self, Self::Multicall3)
@ -218,7 +101,7 @@ impl MulticallVersion {
///
/// `Multicall` can be instantiated asynchronously from the chain ID of the provided client using
/// [`new`] or synchronously by providing a chain ID in [`new_with_chain`]. This, by default, uses
/// [`MULTICALL_ADDRESS`], but can be overridden by providing `Some(address)`.
/// [`constants::MULTICALL_ADDRESS`], but can be overridden by providing `Some(address)`.
/// A list of all the supported chains is available [`here`](https://github.com/mds1/multicall#multicall3-contract-addresses).
///
/// Set the contract's version by using [`version`].
@ -356,17 +239,17 @@ impl<M> fmt::Debug for Multicall<M> {
impl<M: Middleware> Multicall<M> {
/// Creates a new Multicall instance from the provided client. If provided with an `address`,
/// it instantiates the Multicall contract with that address, otherwise it defaults to
/// [`MULTICALL_ADDRESS`].
/// [`constants::MULTICALL_ADDRESS`].
///
/// # Errors
///
/// Returns a [`MulticallError`] if the provider returns an error while getting
/// Returns a [`error::MulticallError`] if the provider returns an error while getting
/// `network_version`.
///
/// # Panics
///
/// If a `None` address is provided and the client's network is
/// [not supported](MULTICALL_SUPPORTED_CHAIN_IDS).
/// [not supported](constants::MULTICALL_SUPPORTED_CHAIN_IDS).
pub async fn new(client: impl Into<Arc<M>>, address: Option<Address>) -> Result<Self, M> {
let client = client.into();
@ -376,12 +259,15 @@ impl<M: Middleware> Multicall<M> {
let address: Address = match address {
Some(addr) => addr,
None => {
let chain_id =
client.get_chainid().await.map_err(ContractError::MiddlewareError)?.as_u64();
if !MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) {
return Err(MulticallError::InvalidChainId(chain_id))
let chain_id = client
.get_chainid()
.await
.map_err(ContractError::from_middleware_error)?
.as_u64();
if !constants::MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) {
return Err(error::MulticallError::InvalidChainId(chain_id))
}
MULTICALL_ADDRESS
constants::MULTICALL_ADDRESS
}
};
@ -398,12 +284,13 @@ impl<M: Middleware> Multicall<M> {
}
/// Creates a new Multicall instance synchronously from the provided client and address or chain
/// ID. Uses the [default multicall address](MULTICALL_ADDRESS) if no address is provided.
/// ID. Uses the [default multicall address](constants::MULTICALL_ADDRESS) if no address is
/// provided.
///
/// # Errors
///
/// Returns a [`MulticallError`] if the provided chain_id is not in the
/// [supported networks](MULTICALL_SUPPORTED_CHAIN_IDS).
/// Returns a [`error::MulticallError`] if the provided chain_id is not in the
/// [supported networks](constants::MULTICALL_SUPPORTED_CHAIN_IDS).
///
/// # Panics
///
@ -421,10 +308,10 @@ impl<M: Middleware> Multicall<M> {
(Some(addr), _) => addr,
(_, Some(chain_id)) => {
let chain_id = chain_id.into();
if !MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) {
return Err(MulticallError::InvalidChainId(chain_id))
if !constants::MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) {
return Err(error::MulticallError::InvalidChainId(chain_id))
}
MULTICALL_ADDRESS
constants::MULTICALL_ADDRESS
}
_ => {
// Can't fetch chain_id from provider since we're not in an async function so we
@ -460,8 +347,9 @@ impl<M: Middleware> Multicall<M> {
/// to use (so you can fit more calls into a single request), and it adds an aggregate3 method
/// so you can specify whether calls are allowed to fail on a per-call basis.
///
/// Note: all these versions are available in the same contract address ([`MULTICALL_ADDRESS`])
/// so changing version just changes the methods used, not the contract address.
/// Note: all these versions are available in the same contract address
/// ([`constants::MULTICALL_ADDRESS`]) so changing version just changes the methods used,
/// not the contract address.
pub fn version(mut self, version: MulticallVersion) -> Self {
self.version = version;
self
@ -673,8 +561,8 @@ impl<M: Middleware> Multicall<M> {
///
/// # Errors
///
/// Returns a [`MulticallError`] if there are any errors in the RPC call or while detokenizing
/// the tokens back to the expected return type.
/// Returns a [`error::MulticallError`] if there are any errors in the RPC call or while
/// detokenizing the tokens back to the expected return type.
///
/// Returns an error if any call failed, even if `allow_failure` was set, or if the return data
/// was empty.
@ -707,7 +595,11 @@ impl<M: Middleware> Multicall<M> {
let results = self.call_raw().await?;
let tokens = results
.into_iter()
.map(|res| res.map_err(MulticallError::CallReverted))
.map(|res| {
res.map_err(|data| {
error::MulticallError::ContractError(ContractError::Revert(data))
})
})
.collect::<Result<_, _>>()?;
T::from_token(Token::Tuple(tokens)).map_err(Into::into)
}
@ -717,8 +609,8 @@ impl<M: Middleware> Multicall<M> {
///
/// # Errors
///
/// Returns a [`MulticallError`] if there are any errors in the RPC call or while detokenizing
/// the tokens back to the expected return type.
/// Returns a [`error::MulticallError`] if there are any errors in the RPC call or while
/// detokenizing the tokens back to the expected return type.
///
/// Returns an error if any call failed, even if `allow_failure` was set, or if the return data
/// was empty.
@ -747,7 +639,9 @@ impl<M: Middleware> Multicall<M> {
.await?
.into_iter()
.map(|res| {
res.map_err(MulticallError::CallReverted)
res.map_err(|data| {
error::MulticallError::ContractError(ContractError::Revert(data))
})
.and_then(|token| T::from_token(token).map_err(Into::into))
})
.collect()
@ -763,7 +657,7 @@ impl<M: Middleware> Multicall<M> {
///
/// # Errors
///
/// Returns a [`MulticallError`] if there are any errors in the RPC call.
/// Returns a [`error::MulticallError`] if there are any errors in the RPC call.
///
/// # Examples
///
@ -824,7 +718,7 @@ impl<M: Middleware> Multicall<M> {
// still do so because of other calls that are in the same multicall
// aggregate.
if !success && !call.allow_failure {
return Err(MulticallError::IllegalRevert)
return Err(error::MulticallError::IllegalRevert)
}
Err(return_data)
@ -849,7 +743,7 @@ impl<M: Middleware> Multicall<M> {
///
/// # Errors
///
/// Returns a [`MulticallError`] if there are any errors in the RPC call.
/// Returns a [`error::MulticallError`] if there are any errors in the RPC call.
///
/// # Examples
///
@ -871,10 +765,9 @@ impl<M: Middleware> Multicall<M> {
MulticallVersion::Multicall3 => self.as_aggregate_3_value().tx,
};
let client: &M = self.contract.client_ref();
client
.send_transaction(tx, self.block.map(Into::into))
.await
.map_err(|e| MulticallError::ContractError(ContractError::MiddlewareError(e)))
client.send_transaction(tx, self.block.map(Into::into)).await.map_err(|e| {
error::MulticallError::ContractError(ContractError::from_middleware_error(e))
})
}
/// v1
@ -984,13 +877,3 @@ impl<M: Middleware> Multicall<M> {
}
}
}
fn decode_error(bytes: &Bytes) -> String {
// Try decoding with "Error(string)" (0x08c379a0)
if bytes.len() >= 4 && bytes[..4] == [0x08, 0xc3, 0x79, 0xa0] {
if let Ok(string) = String::decode(&bytes[4..]) {
return string
}
}
bytes.to_string()
}

View File

@ -1,3 +1,6 @@
//! Contains the `EventStream` type which aids in streaming access to contract
//! events
use crate::LogMeta;
use ethers_core::types::{Log, U256};
use futures_util::{
@ -19,6 +22,7 @@ type MapEvent<'a, R, E> = Box<dyn Fn(Log) -> Result<R, E> + 'a + Send + Sync>;
/// We use this wrapper type instead of `StreamExt::map` in order to preserve
/// information about the filter/subscription's id.
pub struct EventStream<'a, T, R, E> {
/// The stream ID, provided by the RPC server
pub id: U256,
#[pin]
stream: T,
@ -33,6 +37,9 @@ impl<'a, T, R, E> EventStream<'a, T, R, E> {
}
impl<'a, T, R, E> EventStream<'a, T, R, E> {
/// Instantiate a new `EventStream`
///
/// Typically users should not call this directly
pub fn new(id: U256, stream: T, parse: MapEvent<'a, R, E>) -> Self {
Self { id, stream, parse }
}
@ -128,8 +135,10 @@ where
}
}
/// A stream of two items
pub type SelectEither<'a, L, R> = Pin<Box<dyn Stream<Item = Either<L, R>> + 'a>>;
/// Stream for [`EventStream::select`]
#[pin_project]
pub struct SelectEvent<T>(#[pin] T);

View File

@ -722,19 +722,24 @@ mod eth_tests {
.unwrap();
// .call reverts
// don't allow revert -> entire call reverts
multicall.clear_calls().add_call(get_value_reverting_call.clone(), false);
assert!(matches!(
multicall.call::<(String,)>().await.unwrap_err(),
MulticallError::ContractError(_)
));
// don't allow revert
multicall
.clear_calls()
.add_call(get_value_reverting_call.clone(), false)
.add_call(get_value_call.clone(), false);
let res = multicall.call::<(String, String)>().await;
let err = res.unwrap_err();
assert!(err.is_revert());
let message = err.decode_revert::<String>().unwrap();
assert!(message.contains("Multicall3: call failed"));
// allow revert -> call doesn't revert, but returns Err(_) in raw tokens
let expected = Bytes::from_static(b"getValue revert").encode();
multicall.clear_calls().add_call(get_value_reverting_call.clone(), true);
assert_eq!(multicall.call_raw().await.unwrap()[0].as_ref().unwrap_err()[4..], expected[..]);
assert_eq!(
multicall.call::<(String,)>().await.unwrap_err().into_bytes().unwrap()[4..],
multicall.call::<(String,)>().await.unwrap_err().as_revert().unwrap()[4..],
expected[..]
);
@ -774,14 +779,14 @@ mod eth_tests {
// empty revert
let empty_revert = reverting_contract.method::<_, H256>("emptyRevert", ()).unwrap();
multicall.clear_calls().add_call(empty_revert.clone(), true);
assert!(multicall.call::<(String,)>().await.unwrap_err().into_bytes().unwrap().is_empty());
assert!(multicall.call::<(String,)>().await.unwrap_err().as_revert().unwrap().is_empty());
// string revert
let string_revert =
reverting_contract.method::<_, H256>("stringRevert", ("String".to_string())).unwrap();
multicall.clear_calls().add_call(string_revert, true);
assert_eq!(
multicall.call::<(String,)>().await.unwrap_err().into_bytes().unwrap()[4..],
multicall.call::<(String,)>().await.unwrap_err().as_revert().unwrap()[4..],
Bytes::from_static(b"String").encode()[..]
);
@ -789,7 +794,7 @@ mod eth_tests {
let custom_error = reverting_contract.method::<_, H256>("customError", ()).unwrap();
multicall.clear_calls().add_call(custom_error, true);
assert_eq!(
multicall.call::<(Bytes,)>().await.unwrap_err().into_bytes().unwrap()[..],
multicall.call::<(Bytes,)>().await.unwrap_err().as_revert().unwrap()[..],
keccak256("CustomError()")[..4]
);
@ -798,7 +803,8 @@ mod eth_tests {
.method::<_, H256>("customErrorWithData", ("Data".to_string()))
.unwrap();
multicall.clear_calls().add_call(custom_error_with_data, true);
let bytes = multicall.call::<(Bytes,)>().await.unwrap_err().into_bytes().unwrap();
let err = multicall.call::<(Bytes,)>().await.unwrap_err();
let bytes = err.as_revert().unwrap();
assert_eq!(bytes[..4], keccak256("CustomErrorWithData(string)")[..4]);
assert_eq!(bytes[4..], encode(&[Token::String("Data".to_string())]));
}

View File

@ -1,4 +1,4 @@
#![allow(clippy::extra_unused_type_parameters)]
// #![allow(clippy::extra_unused_type_parameters)]
#[cfg(feature = "abigen")]
mod abigen;

View File

@ -143,7 +143,7 @@ impl Tokenizable for String {
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
match token {
Token::String(s) => Ok(s),
other => Err(InvalidOutputType(format!("Expected `String`, got {:?}", other))),
other => Err(InvalidOutputType(format!("Expected `String`, got {other:?}"))),
}
}
@ -156,7 +156,7 @@ impl Tokenizable for Bytes {
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
match token {
Token::Bytes(s) => Ok(s.into()),
other => Err(InvalidOutputType(format!("Expected `Bytes`, got {:?}", other))),
other => Err(InvalidOutputType(format!("Expected `Bytes`, got {other:?}"))),
}
}
@ -169,7 +169,7 @@ impl Tokenizable for bytes::Bytes {
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
match token {
Token::Bytes(s) => Ok(s.into()),
other => Err(InvalidOutputType(format!("Expected `Bytes`, got {:?}", other))),
other => Err(InvalidOutputType(format!("Expected `Bytes`, got {other:?}"))),
}
}
@ -183,7 +183,7 @@ impl Tokenizable for H256 {
match token {
Token::FixedBytes(mut s) => {
if s.len() != 32 {
return Err(InvalidOutputType(format!("Expected `H256`, got {:?}", s)))
return Err(InvalidOutputType(format!("Expected `H256`, got {s:?}")))
}
let mut data = [0; 32];
for (idx, val) in s.drain(..).enumerate() {
@ -191,7 +191,7 @@ impl Tokenizable for H256 {
}
Ok(data.into())
}
other => Err(InvalidOutputType(format!("Expected `H256`, got {:?}", other))),
other => Err(InvalidOutputType(format!("Expected `H256`, got {other:?}"))),
}
}
@ -204,7 +204,7 @@ impl Tokenizable for Address {
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
match token {
Token::Address(data) => Ok(data),
other => Err(InvalidOutputType(format!("Expected `Address`, got {:?}", other))),
other => Err(InvalidOutputType(format!("Expected `Address`, got {other:?}"))),
}
}
@ -217,7 +217,7 @@ impl Tokenizable for bool {
fn from_token(token: Token) -> Result<Self, InvalidOutputType> {
match token {
Token::Bool(data) => Ok(data),
other => Err(InvalidOutputType(format!("Expected `bool`, got {:?}", other))),
other => Err(InvalidOutputType(format!("Expected `bool`, got {other:?}"))),
}
}
fn into_token(self) -> Token {
@ -298,7 +298,7 @@ impl Tokenizable for Vec<u8> {
Token::Bytes(data) => Ok(data),
Token::Array(data) => data.into_iter().map(u8::from_token).collect(),
Token::FixedBytes(data) => Ok(data),
other => Err(InvalidOutputType(format!("Expected `bytes`, got {:?}", other))),
other => Err(InvalidOutputType(format!("Expected `bytes`, got {other:?}"))),
}
}
@ -313,7 +313,7 @@ impl<T: TokenizableItem> Tokenizable for Vec<T> {
Token::FixedArray(tokens) | Token::Array(tokens) => {
tokens.into_iter().map(Tokenizable::from_token).collect()
}
other => Err(InvalidOutputType(format!("Expected `Array`, got {:?}", other))),
other => Err(InvalidOutputType(format!("Expected `Array`, got {other:?}"))),
}
}
@ -338,9 +338,7 @@ impl<const N: usize> Tokenizable for [u8; N] {
arr.copy_from_slice(&bytes);
Ok(arr)
}
other => {
Err(InvalidOutputType(format!("Expected `FixedBytes({})`, got {:?}", N, other)))
}
other => Err(InvalidOutputType(format!("Expected `FixedBytes({N})`, got {other:?}"))),
}
}
@ -372,9 +370,7 @@ impl<T: TokenizableItem + Clone, const N: usize> Tokenizable for [T; N] {
Err(_) => panic!("All elements inserted so the array is full; qed"),
}
}
other => {
Err(InvalidOutputType(format!("Expected `FixedArray({})`, got {:?}", N, other)))
}
other => Err(InvalidOutputType(format!("Expected `FixedArray({N})`, got {other:?}"))),
}
}
@ -483,6 +479,7 @@ mod tests {
let _tuple: (Address, Vec<Vec<u8>>) = assert_detokenize();
let _vec_of_tuple: Vec<(Address, String)> = assert_detokenize();
#[allow(clippy::type_complexity)]
let _vec_of_tuple_5: Vec<(Address, Vec<Vec<u8>>, String, U256, bool)> = assert_detokenize();
}
@ -542,12 +539,12 @@ mod tests {
assert_eq!(data.0[1], 2);
assert_eq!(data.0[2], 3);
assert_eq!(data.0[3], 4);
assert_eq!(data.1, true);
assert!(data.1);
// handle vector of more than one elements
let tokens = vec![Token::Bool(false), Token::Uint(U256::from(13u8))];
let data: (bool, u8) = Detokenize::from_tokens(tokens).unwrap();
assert_eq!(data.0, false);
assert!(!data.0);
assert_eq!(data.1, 13u8);
// handle more than two tuples
@ -559,8 +556,8 @@ mod tests {
assert_eq!((data.0).0[1], 2);
assert_eq!((data.0).0[2], 3);
assert_eq!((data.0).0[3], 4);
assert_eq!((data.0).1, true);
assert_eq!((data.1).0, false);
assert!((data.0).1);
assert!(!(data.1).0);
assert_eq!((data.1).1, 13u8);
// error if no tokens in the vector

View File

@ -95,7 +95,7 @@ impl DsProxy {
Some(addr) => addr,
None => {
let chain_id =
client.get_chainid().await.map_err(ContractError::MiddlewareError)?;
client.get_chainid().await.map_err(ContractError::from_middleware_error)?;
match ADDRESS_BOOK.get(&chain_id) {
Some(addr) => *addr,
None => panic!(
@ -112,8 +112,7 @@ impl DsProxy {
.legacy()
.send()
.await?
.await
.map_err(ContractError::ProviderError)?
.await?
.ok_or(ContractError::ContractNotDeployed)?;
// decode the event log to get the address of the deployed contract.

View File

@ -1,7 +1,10 @@
// Code adapted from: https://github.com/althea-net/guac_rs/tree/master/web3/src/jsonrpc
use base64::{engine::general_purpose, Engine};
use ethers_core::types::U256;
use ethers_core::{
abi::AbiDecode,
types::{Bytes, U256},
};
use serde::{
de::{self, MapAccess, Unexpected, Visitor},
Deserialize, Serialize,
@ -21,6 +24,48 @@ pub struct JsonRpcError {
pub data: Option<Value>,
}
/// Recursively traverses the value, looking for hex data that it can extract
/// Inspired by ethers-js logic:
/// https://github.com/ethers-io/ethers.js/blob/9f990c57f0486728902d4b8e049536f2bb3487ee/packages/providers/src.ts/json-rpc-provider.ts#L25-L53
fn spelunk_revert(value: &Value) -> Option<Bytes> {
match value {
Value::String(s) => s.parse().ok(),
Value::Object(o) => o.values().flat_map(spelunk_revert).next(),
_ => None,
}
}
impl JsonRpcError {
/// Determine if the error output of the `eth_call` RPC request is a revert
///
/// Note that this may return false positives if called on an error from
/// other RPC requests
pub fn is_revert(&self) -> bool {
// Ganache says "revert" not "reverted"
self.message.contains("revert")
}
/// Attempt to extract revert data from the JsonRpcError be recursively
/// traversing the error's data field
///
/// This returns the first hex it finds in the data object, and its
/// behavior may change with `serde_json` internal changes.
///
/// If no hex object is found, it will return an empty bytes IFF the error
/// is a revert
///
/// Inspired by ethers-js logic:
/// <https://github.com/ethers-io/ethers.js/blob/9f990c57f0486728902d4b8e049536f2bb3487ee/packages/providers/src.ts/json-rpc-provider.ts#L25-L53>
pub fn as_revert_data(&self) -> Option<Bytes> {
self.is_revert().then(|| self.data.as_ref().and_then(spelunk_revert).unwrap_or_default())
}
/// Decode revert data (if any) into a decodeable type
pub fn decode_revert_data<E: AbiDecode>(&self) -> Option<E> {
E::decode(&self.as_revert_data()?).ok()
}
}
impl fmt::Display for JsonRpcError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "(code: {}, message: {}, data: {:?})", self.code, self.message, self.data)

View File

@ -4,7 +4,6 @@ use ethers::{
};
use eyre::Result;
use std::sync::Arc;
use tokio;
const HTTP_URL: &str = "https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27";
const V3FACTORY_ADDRESS: &str = "0x1F98431c8aD98523631AE4a59f267346ea31F984";
@ -40,8 +39,7 @@ async fn main() -> Result<()> {
let tick_spacing = U256::from_big_endian(&log.data[29..32]);
let pool = Address::from(&log.data[44..64].try_into()?);
println!(
"pool = {}, token0 = {}, token1 = {}, fee = {}, spacing = {}",
pool, token0, token1, fee_tier, tick_spacing,
"pool = {pool}, token0 = {token0}, token1 = {token1}, fee = {fee_tier}, spacing = {tick_spacing}"
);
}
Ok(())