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:
parent
ee5e3e52c0
commit
2090bf560e
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
]
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
#![allow(missing_docs)]
|
||||
use ethers_contract_derive::abigen;
|
||||
|
||||
abigen!(Multicall3, "src/multicall/multicall_abi.json");
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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())]));
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#![allow(clippy::extra_unused_type_parameters)]
|
||||
// #![allow(clippy::extra_unused_type_parameters)]
|
||||
|
||||
#[cfg(feature = "abigen")]
|
||||
mod abigen;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(())
|
||||
|
|
Loading…
Reference in New Issue