Skip to main content

alloy_contract/
error.rs

1use alloy_dyn_abi::Error as AbiError;
2use alloy_primitives::{Bytes, Selector};
3use alloy_provider::{MulticallError, PendingTransactionError};
4use alloy_sol_types::{SolError, SolInterface};
5use alloy_transport::{RpcError, TransportError, TransportErrorKind};
6use serde_json::value::RawValue;
7use thiserror::Error;
8
9/// Dynamic contract result type.
10pub type Result<T, E = Error> = core::result::Result<T, E>;
11
12/// Error when interacting with contracts.
13#[derive(Debug, Error)]
14pub enum Error {
15    /// Unknown function referenced.
16    #[error("unknown function: function {0} does not exist")]
17    UnknownFunction(String),
18    /// Unknown function selector referenced.
19    #[error("unknown function: function with selector {0} does not exist")]
20    UnknownSelector(Selector),
21    /// Called `deploy` with a transaction that is not a deployment transaction.
22    #[error("transaction is not a deployment transaction")]
23    NotADeploymentTransaction,
24    /// Contract was not deployed: either the deployment transaction reverted or `contractAddress`
25    /// was not found in the receipt.
26    #[error("contract not deployed: deployment transaction failed")]
27    ContractNotDeployed,
28    /// The contract returned no data.
29    #[error("contract call to `{0}` returned no data (\"0x\"); the called address might not be a contract")]
30    ZeroData(String, #[source] AbiError),
31    /// An error occurred ABI encoding or decoding.
32    #[error(transparent)]
33    AbiError(#[from] AbiError),
34    /// An error occurred interacting with a contract over RPC.
35    #[error(transparent)]
36    TransportError(#[from] TransportError),
37    /// An error occurred while waiting for a pending transaction.
38    #[error(transparent)]
39    PendingTransactionError(#[from] PendingTransactionError),
40}
41
42impl From<alloy_sol_types::Error> for Error {
43    #[inline]
44    fn from(e: alloy_sol_types::Error) -> Self {
45        Self::AbiError(e.into())
46    }
47}
48
49impl From<MulticallError> for Error {
50    fn from(error: MulticallError) -> Self {
51        match error {
52            MulticallError::TransportError(error) => Self::TransportError(error),
53            MulticallError::DecodeError(error) => error.into(),
54            error => Self::TransportError(TransportErrorKind::custom_str(&error.to_string())),
55        }
56    }
57}
58
59impl Error {
60    #[cold]
61    pub(crate) fn decode(name: &str, data: &[u8], error: AbiError) -> Self {
62        if data.is_empty() {
63            let name = name.split('(').next().unwrap_or(name);
64            return Self::ZeroData(name.to_string(), error);
65        }
66        Self::AbiError(error)
67    }
68
69    /// Return the revert data in case the call reverted.
70    pub fn as_revert_data(&self) -> Option<Bytes> {
71        if let Self::TransportError(e) = self {
72            return e.as_error_resp().and_then(|e| e.as_revert_data());
73        }
74
75        None
76    }
77
78    /// Attempts to decode the revert data into one of the custom errors in [`SolInterface`].
79    ///
80    /// Returns an enum container type consisting of the custom errors defined in the interface.
81    ///
82    /// None is returned if the revert data is empty or if the data could not be decoded into one of
83    /// the custom errors defined in the interface.
84    ///
85    /// # Examples
86    ///
87    /// ```no_run
88    /// use alloy_provider::ProviderBuilder;
89    /// use alloy_sol_types::sol;
90    ///
91    /// sol! {
92    ///     #[derive(Debug, PartialEq, Eq)]
93    ///     #[sol(rpc, bytecode = "694207")]
94    ///     contract ThrowsError {
95    ///         error SomeCustomError(uint64 a);
96    ///         error AnotherError(uint64 b);
97    ///
98    ///         function error(uint64 a) external {
99    ///             revert SomeCustomError(a);
100    ///         }
101    ///     }
102    /// }
103    ///
104    /// #[tokio::main]
105    /// async fn main() {
106    ///     let provider = ProviderBuilder::new().connect_anvil_with_wallet();
107    ///
108    ///     let throws_err = ThrowsError::deploy(provider).await.unwrap();
109    ///
110    ///     let err = throws_err.error(42).call().await.unwrap_err();
111    ///
112    ///     let custom_err =
113    ///         err.as_decoded_interface_error::<ThrowsError::ThrowsErrorErrors>().unwrap();
114    ///
115    ///     // Handle the custom error enum
116    ///     match custom_err {
117    ///         ThrowsError::ThrowsErrorErrors::SomeCustomError(a) => { /* handle error */ }
118    ///         ThrowsError::ThrowsErrorErrors::AnotherError(b) => { /* handle error */ }
119    ///     }
120    /// }
121    /// ```
122    pub fn as_decoded_interface_error<E: SolInterface>(&self) -> Option<E> {
123        self.as_revert_data().and_then(|data| E::abi_decode(&data).ok())
124    }
125
126    /// Try to decode a contract error into a specific Solidity error interface.
127    /// If the error cannot be decoded or it is not a contract error, return the original error.
128    ///
129    /// Example usage:
130    ///
131    /// ```ignore
132    /// sol! {
133    ///    library ErrorLib {
134    ///       error SomeError(uint256 code);
135    ///    }
136    /// }
137    ///
138    /// // call a contract that may return an error with the SomeError interface
139    /// let returndata = match myContract.call().await {
140    ///    Ok(returndata) => returndata,
141    ///    Err(err) => {
142    ///         let decoded_error = err.try_decode_into_interface_error::<ErrorLib::ErrorLibError>()?;
143    ///        // handle the decoded error however you want; for example, return it
144    ///         return Err(decoded_error);
145    ///    },
146    /// }
147    /// ```
148    ///
149    /// See also [`Self::as_decoded_interface_error`] for more details.
150    pub fn try_decode_into_interface_error<I: SolInterface>(self) -> Result<I, Self> {
151        self.as_decoded_interface_error::<I>().ok_or(self)
152    }
153
154    /// Decode the revert data into a custom [`SolError`] type.
155    ///
156    /// Returns an instance of the custom error type if decoding was successful, otherwise None.
157    ///
158    /// # Examples
159    ///
160    /// ```no_run
161    /// use alloy_provider::ProviderBuilder;
162    /// use alloy_sol_types::sol;
163    /// use ThrowsError::SomeCustomError;
164    /// sol! {
165    ///     #[derive(Debug, PartialEq, Eq)]
166    ///     #[sol(rpc, bytecode = "694207")]
167    ///     contract ThrowsError {
168    ///         error SomeCustomError(uint64 a);
169    ///         error AnotherError(uint64 b);
170    ///
171    ///         function error(uint64 a) external {
172    ///             revert SomeCustomError(a);
173    ///         }
174    ///     }
175    /// }
176    ///
177    /// #[tokio::main]
178    /// async fn main() {
179    ///     let provider = ProviderBuilder::new().connect_anvil_with_wallet();
180    ///
181    ///     let throws_err = ThrowsError::deploy(provider).await.unwrap();
182    ///
183    ///     let err = throws_err.error(42).call().await.unwrap_err();
184    ///
185    ///     let custom_err = err.as_decoded_error::<SomeCustomError>().unwrap();
186    ///
187    ///     assert_eq!(custom_err, SomeCustomError { a: 42 });
188    /// }
189    /// ```
190    pub fn as_decoded_error<E: SolError>(&self) -> Option<E> {
191        self.as_revert_data().and_then(|data| E::abi_decode(&data).ok())
192    }
193}
194
195/// The result of trying to parse a transport error into a specific interface.
196#[derive(Debug)]
197pub enum TryParseTransportErrorResult<I: SolInterface> {
198    /// The error was successfully decoded into the specified interface.
199    Decoded(I),
200    /// The error was not decoded but the revert data was extracted.
201    UnknownSelector(Bytes),
202    /// The error was not decoded and the revert data was not extracted.
203    Original(RpcError<TransportErrorKind, Box<RawValue>>),
204}
205
206/// Extension trait for TransportError parsing capabilities
207pub trait TransportErrorExt {
208    /// Attempts to parse a transport error into a specific interface.
209    fn try_parse_transport_error<I: SolInterface>(self) -> TryParseTransportErrorResult<I>;
210}
211
212impl TransportErrorExt for TransportError {
213    fn try_parse_transport_error<I: SolInterface>(self) -> TryParseTransportErrorResult<I> {
214        let revert_data = self.as_error_resp().and_then(|e| e.as_revert_data());
215        if let Some(decoded) =
216            revert_data.as_ref().and_then(|data| I::abi_decode(data.as_ref()).ok())
217        {
218            return TryParseTransportErrorResult::Decoded(decoded);
219        }
220
221        if let Some(decoded) = revert_data {
222            return TryParseTransportErrorResult::UnknownSelector(decoded);
223        }
224        TryParseTransportErrorResult::Original(self)
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn converts_multicall_transport_error() {
234        let error = MulticallError::TransportError(TransportErrorKind::backend_gone());
235
236        assert!(matches!(Error::from(error), Error::TransportError(_)));
237    }
238
239    #[test]
240    fn converts_multicall_decode_error() {
241        let error = MulticallError::DecodeError(alloy_sol_types::Error::Overrun);
242
243        assert!(matches!(Error::from(error), Error::AbiError(_)));
244    }
245
246    #[test]
247    fn converts_other_multicall_errors_to_transport_errors() {
248        let error = Error::from(MulticallError::NoReturnData);
249
250        assert!(matches!(error, Error::TransportError(_)));
251        assert_eq!(error.to_string(), "no return data");
252    }
253}