diff --git a/CHANGELOG.md b/CHANGELOG.md index f7c22e40..255d604d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/ethers-contract/src/base.rs b/ethers-contract/src/base.rs index 6f0c4395..be648b52 100644 --- a/ethers-contract/src/base.rs +++ b/ethers-contract/src/base.rs @@ -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>( &self, signature: Selector, diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index ad931078..016ba73e 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -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 { 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 { ContractNotDeployed, } +impl ContractError { + /// 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(&self) -> Option { + 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 From for ContractError { + 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> { - 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>> + Debug { let call = self.call_raw_bytes(); call.map(move |res: Result| { - 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) } } diff --git a/ethers-contract/src/error.rs b/ethers-contract/src/error.rs index cfac0651..b0f4f707 100644 --- a/ethers-contract/src/error.rs +++ b/ethers-contract/src/error.rs @@ -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::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 { + // This will return none if selector mismatch. + ::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" + ); + } +} diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs index df529e6d..1f771f79 100644 --- a/ethers-contract/src/event.rs +++ b/ethers-contract/src/event.rs @@ -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| { diff --git a/ethers-contract/src/factory.rs b/ethers-contract/src/factory.rs index d3248150..6aa08eab 100644 --- a/ethers-contract/src/factory.rs +++ b/ethers-contract/src/factory.rs @@ -79,6 +79,7 @@ where self } + /// Sets the block at which RPC requests are made pub fn block>(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>(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) -> Result, ContractError> where B: Clone, diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index 002f07c9..238ae951 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -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 diff --git a/ethers-contract/src/multicall/constants.rs b/ethers-contract/src/multicall/constants.rs new file mode 100644 index 00000000..7f111061 --- /dev/null +++ b/ethers-contract/src/multicall/constants.rs @@ -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: +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 + ] +}; diff --git a/ethers-contract/src/multicall/contract.rs b/ethers-contract/src/multicall/contract.rs new file mode 100644 index 00000000..a18fdb4e --- /dev/null +++ b/ethers-contract/src/multicall/contract.rs @@ -0,0 +1,4 @@ +#![allow(missing_docs)] +use ethers_contract_derive::abigen; + +abigen!(Multicall3, "src/multicall/multicall_abi.json"); diff --git a/ethers-contract/src/multicall/error.rs b/ethers-contract/src/multicall/error.rs new file mode 100644 index 00000000..e9ec06b9 --- /dev/null +++ b/ethers-contract/src/multicall/error.rs @@ -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 { + /// Contract call returned an error + #[error(transparent)] + ContractError(#[from] ContractError), + + /// 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 From for MulticallError { + fn from(value: abi::Error) -> Self { + Self::ContractError(ContractError::DecodingError(value)) + } +} + +impl From for MulticallError { + fn from(value: InvalidOutputType) -> Self { + Self::ContractError(ContractError::DetokenizationError(value)) + } +} + +impl MulticallError { + /// Convert a `MulticallError` to a the underlying error if possible. + pub fn as_contract_error(&self) -> Option<&ContractError> { + 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(&self) -> Option { + self.as_revert().and_then(|data| Err::decode_with_selector(data)) + } +} diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs index 85af2faa..14f7cd55 100644 --- a/ethers-contract/src/multicall/mod.rs +++ b/ethers-contract/src/multicall/mod.rs @@ -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: -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>` -pub type Result = StdResult>; +pub type Result = StdResult>; -#[derive(Debug, thiserror::Error)] -pub enum MulticallError { - #[error(transparent)] - ContractError(#[from] ContractError), - - #[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 From for MulticallError { - fn from(value: ethers_core::abi::Error) -> Self { - Self::ContractError(ContractError::DecodingError(value)) - } -} - -impl From for MulticallError { - fn from(value: InvalidOutputType) -> Self { - Self::ContractError(ContractError::DetokenizationError(value)) - } -} - -impl MulticallError { - pub fn into_bytes(self) -> Result { - 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 { - 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 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 fmt::Debug for Multicall { impl Multicall { /// 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>, address: Option
) -> Result { let client = client.into(); @@ -376,12 +259,15 @@ impl Multicall { 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 Multicall { } /// 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 Multicall { (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 Multicall { /// 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 Multicall { /// /// # 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 Multicall { 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::>()?; T::from_token(Token::Tuple(tokens)).map_err(Into::into) } @@ -717,8 +609,8 @@ impl Multicall { /// /// # 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,8 +639,10 @@ impl Multicall { .await? .into_iter() .map(|res| { - res.map_err(MulticallError::CallReverted) - .and_then(|token| T::from_token(token).map_err(Into::into)) + 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 Multicall { /// /// # 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 Multicall { // 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 Multicall { /// /// # 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 Multicall { 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 Multicall { } } } - -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() -} diff --git a/ethers-contract/src/stream.rs b/ethers-contract/src/stream.rs index e258ea4c..cbd5685d 100644 --- a/ethers-contract/src/stream.rs +++ b/ethers-contract/src/stream.rs @@ -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 Result + '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> + 'a>>; +/// Stream for [`EventStream::select`] #[pin_project] pub struct SelectEvent(#[pin] T); diff --git a/ethers-contract/tests/it/contract.rs b/ethers-contract/tests/it/contract.rs index e33c522e..a8cc689c 100644 --- a/ethers-contract/tests/it/contract.rs +++ b/ethers-contract/tests/it/contract.rs @@ -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::().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())])); } diff --git a/ethers-contract/tests/it/main.rs b/ethers-contract/tests/it/main.rs index dda43f81..bdf63ef0 100644 --- a/ethers-contract/tests/it/main.rs +++ b/ethers-contract/tests/it/main.rs @@ -1,4 +1,4 @@ -#![allow(clippy::extra_unused_type_parameters)] +// #![allow(clippy::extra_unused_type_parameters)] #[cfg(feature = "abigen")] mod abigen; diff --git a/ethers-core/src/abi/tokens.rs b/ethers-core/src/abi/tokens.rs index e6f11b88..38d1a284 100644 --- a/ethers-core/src/abi/tokens.rs +++ b/ethers-core/src/abi/tokens.rs @@ -143,7 +143,7 @@ impl Tokenizable for String { fn from_token(token: Token) -> Result { 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 { 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 { 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 { 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 { 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 { 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 Tokenizable for Vec { 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 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 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>) = assert_detokenize(); let _vec_of_tuple: Vec<(Address, String)> = assert_detokenize(); + #[allow(clippy::type_complexity)] let _vec_of_tuple_5: Vec<(Address, Vec>, 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 diff --git a/ethers-middleware/src/transformer/ds_proxy/mod.rs b/ethers-middleware/src/transformer/ds_proxy/mod.rs index 7251ab2b..d97e7a16 100644 --- a/ethers-middleware/src/transformer/ds_proxy/mod.rs +++ b/ethers-middleware/src/transformer/ds_proxy/mod.rs @@ -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. diff --git a/ethers-providers/src/rpc/transports/common.rs b/ethers-providers/src/rpc/transports/common.rs index 02591928..9caab6b1 100644 --- a/ethers-providers/src/rpc/transports/common.rs +++ b/ethers-providers/src/rpc/transports/common.rs @@ -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, } +/// 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 { + 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: + /// + pub fn as_revert_data(&self) -> Option { + 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(&self) -> Option { + 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) diff --git a/examples/events/examples/filtering.rs b/examples/events/examples/filtering.rs index 5791934e..d1ab9092 100644 --- a/examples/events/examples/filtering.rs +++ b/examples/events/examples/filtering.rs @@ -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(())