use alloy_primitives::{B256, Selector};
use alloy_sol_types::{SolError, SolInterface};
use thiserror::Error;
use tronz_primitives::Bytes;
use tronz_provider::{PendingTransactionError, ProviderError, types::TransactionInfo};
pub type Result<T, E = ContractError> = core::result::Result<T, E>;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ContractError {
#[error(transparent)]
Provider(#[from] ProviderError),
#[error("ABI error: {0}")]
Abi(#[from] alloy_dyn_abi::Error),
#[error(transparent)]
AbiMetadata(#[from] tronz_abi::TronAbiConversionError),
#[error("unknown function: `{0}`")]
UnknownFunction(String),
#[error("unknown function selector: {0}")]
UnknownSelector(Selector),
#[error("contract call to `{0}` returned no data; the address might not be a contract")]
ZeroData(String, #[source] alloy_dyn_abi::Error),
#[error("contract call reverted")]
Revert(Bytes),
#[error("unknown event topic: {0}")]
UnknownEvent(B256),
#[error("contract not deployed: deployment transaction did not produce a contract address")]
ContractNotDeployed,
#[error("timed out waiting for transaction confirmation")]
ConfirmationTimeout,
#[error("transaction confirmed but execution failed: {:?}", .0.contract_result)]
ExecutionFailed(Box<TransactionInfo>),
}
impl From<alloy_sol_types::Error> for ContractError {
fn from(e: alloy_sol_types::Error) -> Self {
Self::Abi(e.into())
}
}
impl From<PendingTransactionError> for ContractError {
fn from(e: PendingTransactionError) -> Self {
match e {
PendingTransactionError::Transport(e) => Self::Provider(e),
PendingTransactionError::ConfirmationTimeout => Self::ConfirmationTimeout,
PendingTransactionError::Reverted(info) => Self::ExecutionFailed(info),
_ => {
Self::Provider(ProviderError::local_usage_str("unknown pending transaction error"))
}
}
}
}
impl ContractError {
pub fn as_revert_data(&self) -> Option<&Bytes> {
if let Self::Revert(data) = self { Some(data) } else { None }
}
pub fn as_decoded_error<E: SolError>(&self) -> Option<E> {
self.as_revert_data().and_then(|data| E::abi_decode(data).ok())
}
pub fn as_decoded_interface_error<I: SolInterface>(&self) -> Option<I> {
self.as_revert_data().and_then(|data| I::abi_decode(data).ok())
}
pub(crate) fn decode_err(name: &str, data: &[u8], error: alloy_dyn_abi::Error) -> Self {
if data.is_empty() {
let short = name.split('(').next().unwrap_or(name);
return Self::ZeroData(short.to_string(), error);
}
Self::Abi(error)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_err() -> alloy_dyn_abi::Error {
alloy_dyn_abi::Error::TypeMismatch { expected: "uint256".into(), actual: "bytes".into() }
}
#[test]
fn empty_data_yields_zero_data() {
let err = ContractError::decode_err("balanceOf", &[], dummy_err());
assert!(
matches!(&err, ContractError::ZeroData(name, _) if name == "balanceOf"),
"got {err:?}"
);
}
#[test]
fn full_signature_is_stripped_to_name() {
let err = ContractError::decode_err("transfer(address,uint256)", &[], dummy_err());
assert!(
matches!(&err, ContractError::ZeroData(name, _) if name == "transfer"),
"got {err:?}"
);
}
#[test]
fn non_empty_data_yields_abi_error() {
let err = ContractError::decode_err("balanceOf", &[0xde, 0xad], dummy_err());
assert!(matches!(err, ContractError::Abi(_)), "got {err:?}");
}
}