use thiserror::Error;
#[derive(Error, Debug)]
pub enum SuiError {
#[error("RPC error: {0}")]
RpcError(String),
#[error("Object already used: {0}")]
ObjectUsed(String),
#[error("State proof verification failed: {0}")]
StateProofFailed(String),
#[error("Event proof verification failed: {0}")]
EventProofFailed(String),
#[error("Checkpoint certification failed: {0}")]
CheckpointFailed(String),
#[error("Transaction failed: {0}")]
TransactionFailed(String),
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Transaction confirmation timeout after {timeout_ms}ms: {tx_digest}")]
ConfirmationTimeout { tx_digest: String, timeout_ms: u64 },
#[error("Chain reorg detected at checkpoint {checkpoint}: anchor may be invalid")]
ReorgDetected { checkpoint: u64 },
#[error("Network mismatch: expected chain_id {expected}, got {actual}")]
NetworkMismatch { expected: String, actual: String },
#[error(transparent)]
CoreError(#[from] csv_adapter_core::AdapterError),
}
impl SuiError {
pub fn is_transient(&self) -> bool {
match self {
SuiError::RpcError(_)
| SuiError::ConfirmationTimeout { .. }
| SuiError::TransactionFailed(_) => true,
SuiError::ObjectUsed(_)
| SuiError::StateProofFailed(_)
| SuiError::EventProofFailed(_)
| SuiError::CheckpointFailed(_)
| SuiError::SerializationError(_)
| SuiError::ReorgDetected { .. }
| SuiError::NetworkMismatch { .. }
| SuiError::CoreError(_) => false,
}
}
pub fn timeout(tx_digest: &str, timeout_ms: u64) -> Self {
SuiError::ConfirmationTimeout {
tx_digest: tx_digest.to_string(),
timeout_ms,
}
}
pub fn reorg(checkpoint: u64) -> Self {
SuiError::ReorgDetected { checkpoint }
}
}
impl From<Box<dyn std::error::Error + Send + Sync>> for SuiError {
fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
SuiError::RpcError(err.to_string())
}
}
impl From<SuiError> for csv_adapter_core::AdapterError {
fn from(err: SuiError) -> Self {
match err {
SuiError::CoreError(e) => e,
SuiError::RpcError(msg) | SuiError::TransactionFailed(msg) => {
csv_adapter_core::AdapterError::NetworkError(msg)
}
SuiError::ObjectUsed(msg) => csv_adapter_core::AdapterError::InvalidSeal(msg),
SuiError::StateProofFailed(msg) | SuiError::EventProofFailed(msg) => {
csv_adapter_core::AdapterError::InclusionProofFailed(msg)
}
SuiError::CheckpointFailed(msg) => csv_adapter_core::AdapterError::NetworkError(msg),
SuiError::SerializationError(msg) => csv_adapter_core::AdapterError::InvalidSeal(msg),
SuiError::ConfirmationTimeout {
tx_digest,
timeout_ms,
} => csv_adapter_core::AdapterError::NetworkError(format!(
"Timeout waiting for tx {} after {}ms",
tx_digest, timeout_ms
)),
SuiError::ReorgDetected { checkpoint } => csv_adapter_core::AdapterError::ReorgInvalid(
format!("Reorg at checkpoint {}", checkpoint),
),
sui_err => csv_adapter_core::AdapterError::NetworkError(format!("{}", sui_err)),
}
}
}
pub type SuiResult<T> = Result<T, SuiError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_transient_errors() {
assert!(SuiError::RpcError("connection refused".to_string()).is_transient());
assert!(SuiError::ConfirmationTimeout {
tx_digest: "abc".to_string(),
timeout_ms: 30000
}
.is_transient());
assert!(SuiError::TransactionFailed("execution failed".to_string()).is_transient());
}
#[test]
fn test_non_transient_errors() {
assert!(!SuiError::ObjectUsed("object consumed".to_string()).is_transient());
assert!(!SuiError::StateProofFailed("invalid proof".to_string()).is_transient());
assert!(!SuiError::ReorgDetected { checkpoint: 100 }.is_transient());
}
#[test]
fn test_error_conversion() {
let sui_err = SuiError::StateProofFailed("bad proof".to_string());
let core_err: csv_adapter_core::AdapterError = sui_err.into();
assert!(matches!(
core_err,
csv_adapter_core::AdapterError::InclusionProofFailed(_)
));
}
}