Skip to main content

csv_adapter_sui/
error.rs

1//! Sui adapter error types
2//!
3//! This module provides a comprehensive error taxonomy for the Sui adapter,
4//! with chain-specific error variants and recovery guidance.
5
6use thiserror::Error;
7
8/// Comprehensive error types for the Sui adapter.
9///
10/// Each variant includes context for debugging and recovery guidance.
11#[derive(Error, Debug)]
12pub enum SuiError {
13    /// Error during RPC communication with Sui node.
14    /// Recovery: Retry with backoff, switch to fallback RPC endpoint.
15    #[error("RPC error: {0}")]
16    RpcError(String),
17
18    /// Seal object has already been consumed.
19    /// Recovery: This is a fatal error for seal replay attempts. Check object state.
20    #[error("Object already used: {0}")]
21    ObjectUsed(String),
22
23    /// State proof verification failed (object existence/ownership).
24    /// Recovery: Re-fetch proof from different RPC endpoint, check for reorg.
25    #[error("State proof verification failed: {0}")]
26    StateProofFailed(String),
27
28    /// Event proof verification failed (event emission verification).
29    /// Recovery: Re-verify transaction, check event index and data.
30    #[error("Event proof verification failed: {0}")]
31    EventProofFailed(String),
32
33    /// Checkpoint certification verification failed.
34    /// Recovery: Check validator signatures, verify epoch boundaries.
35    #[error("Checkpoint certification failed: {0}")]
36    CheckpointFailed(String),
37
38    /// Transaction submission or execution failed.
39    /// Recovery: Check transaction simulation error, adjust gas parameters.
40    #[error("Transaction failed: {0}")]
41    TransactionFailed(String),
42
43    /// Error during serialization/deserialization.
44    /// Recovery: This is a programming error. Check data format compatibility.
45    #[error("Serialization error: {0}")]
46    SerializationError(String),
47
48    /// Timeout while waiting for transaction confirmation.
49    /// Recovery: Resubmit transaction with higher gas, check mempool status.
50    #[error("Transaction confirmation timeout after {timeout_ms}ms: {tx_digest}")]
51    ConfirmationTimeout { tx_digest: String, timeout_ms: u64 },
52
53    /// Chain reorg detected affecting anchor validity.
54    /// Recovery: Re-publish commitment at new chain tip.
55    #[error("Chain reorg detected at checkpoint {checkpoint}: anchor may be invalid")]
56    ReorgDetected { checkpoint: u64 },
57
58    /// Network mismatch (e.g., mainnet seal on testnet).
59    /// Recovery: Ensure network configuration matches chain ID.
60    #[error("Network mismatch: expected chain_id {expected}, got {actual}")]
61    NetworkMismatch { expected: String, actual: String },
62
63    /// Core adapter error from csv-adapter-core.
64    #[error(transparent)]
65    CoreError(#[from] csv_adapter_core::AdapterError),
66}
67
68impl SuiError {
69    /// Returns true if this error is potentially transient and should be retried.
70    pub fn is_transient(&self) -> bool {
71        match self {
72            SuiError::RpcError(_)
73            | SuiError::ConfirmationTimeout { .. }
74            | SuiError::TransactionFailed(_) => true,
75            SuiError::ObjectUsed(_)
76            | SuiError::StateProofFailed(_)
77            | SuiError::EventProofFailed(_)
78            | SuiError::CheckpointFailed(_)
79            | SuiError::SerializationError(_)
80            | SuiError::ReorgDetected { .. }
81            | SuiError::NetworkMismatch { .. }
82            | SuiError::CoreError(_) => false,
83        }
84    }
85
86    /// Construct an error for transaction timeout
87    pub fn timeout(tx_digest: &str, timeout_ms: u64) -> Self {
88        SuiError::ConfirmationTimeout {
89            tx_digest: tx_digest.to_string(),
90            timeout_ms,
91        }
92    }
93
94    /// Construct an error for chain reorg
95    pub fn reorg(checkpoint: u64) -> Self {
96        SuiError::ReorgDetected { checkpoint }
97    }
98}
99
100impl From<Box<dyn std::error::Error + Send + Sync>> for SuiError {
101    fn from(err: Box<dyn std::error::Error + Send + Sync>) -> Self {
102        SuiError::RpcError(err.to_string())
103    }
104}
105
106impl From<SuiError> for csv_adapter_core::AdapterError {
107    fn from(err: SuiError) -> Self {
108        match err {
109            SuiError::CoreError(e) => e,
110            SuiError::RpcError(msg) | SuiError::TransactionFailed(msg) => {
111                csv_adapter_core::AdapterError::NetworkError(msg)
112            }
113            SuiError::ObjectUsed(msg) => csv_adapter_core::AdapterError::InvalidSeal(msg),
114            SuiError::StateProofFailed(msg) | SuiError::EventProofFailed(msg) => {
115                csv_adapter_core::AdapterError::InclusionProofFailed(msg)
116            }
117            SuiError::CheckpointFailed(msg) => csv_adapter_core::AdapterError::NetworkError(msg),
118            SuiError::SerializationError(msg) => csv_adapter_core::AdapterError::InvalidSeal(msg),
119            SuiError::ConfirmationTimeout {
120                tx_digest,
121                timeout_ms,
122            } => csv_adapter_core::AdapterError::NetworkError(format!(
123                "Timeout waiting for tx {} after {}ms",
124                tx_digest, timeout_ms
125            )),
126            SuiError::ReorgDetected { checkpoint } => csv_adapter_core::AdapterError::ReorgInvalid(
127                format!("Reorg at checkpoint {}", checkpoint),
128            ),
129            sui_err => csv_adapter_core::AdapterError::NetworkError(format!("{}", sui_err)),
130        }
131    }
132}
133
134/// Result type for Sui adapter operations
135pub type SuiResult<T> = Result<T, SuiError>;
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_transient_errors() {
143        assert!(SuiError::RpcError("connection refused".to_string()).is_transient());
144        assert!(SuiError::ConfirmationTimeout {
145            tx_digest: "abc".to_string(),
146            timeout_ms: 30000
147        }
148        .is_transient());
149        assert!(SuiError::TransactionFailed("execution failed".to_string()).is_transient());
150    }
151
152    #[test]
153    fn test_non_transient_errors() {
154        assert!(!SuiError::ObjectUsed("object consumed".to_string()).is_transient());
155        assert!(!SuiError::StateProofFailed("invalid proof".to_string()).is_transient());
156        assert!(!SuiError::ReorgDetected { checkpoint: 100 }.is_transient());
157    }
158
159    #[test]
160    fn test_error_conversion() {
161        let sui_err = SuiError::StateProofFailed("bad proof".to_string());
162        let core_err: csv_adapter_core::AdapterError = sui_err.into();
163        assert!(matches!(
164            core_err,
165            csv_adapter_core::AdapterError::InclusionProofFailed(_)
166        ));
167    }
168}