Skip to main content

xmtp_common/
error_code.rs

1//! Unique error codes for cross-binding error identification.
2//!
3//! This module provides the `ErrorCode` trait which gives errors a stable,
4//! machine-readable identifier that can be used across language bindings.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use xmtp_common::ErrorCode;
10//! use thiserror::Error;
11//!
12//! #[derive(Debug, Error, ErrorCode)]
13//! pub enum GroupError {
14//!     #[error("Group not found")]
15//!     NotFound,  // Returns "GroupError::NotFound"
16//!
17//!     #[error("Storage error: {0}")]
18//!     #[error_code(inherit)]  // Delegates to StorageError::error_code()
19//!     Storage(#[from] StorageError),
20//! }
21//! ```
22
23/// A trait for errors that have a unique, stable error code.
24///
25/// Error codes are formatted as `"TypeName::VariantName"` for enum variants
26/// or `"TypeName"` for struct errors.
27///
28/// Use `#[derive(ErrorCode)]` from `xmtp_macro` to automatically implement this trait.
29pub trait ErrorCode: std::error::Error {
30    /// Returns the unique error code for this error.
31    ///
32    /// The code is a static string in the format `"TypeName::VariantName"`.
33    fn error_code(&self) -> &'static str;
34}
35
36impl<E: ErrorCode> ErrorCode for Box<E> {
37    fn error_code(&self) -> &'static str {
38        (**self).error_code()
39    }
40}
41
42impl<E: ErrorCode> ErrorCode for &E {
43    fn error_code(&self) -> &'static str {
44        (*self).error_code()
45    }
46}
47
48// Derived implementations for xmtp_cryptography errors using remote targets.
49// These mirror the remote types solely to drive the ErrorCode derive.
50#[allow(dead_code)]
51mod cryptography_error_codes {
52    #[derive(xmtp_common::ErrorCode)]
53    #[error_code(remote = "xmtp_cryptography::signature::SignatureError")]
54    enum SignatureError {
55        BadAddressFormat(()),
56        BadSignatureFormat(()),
57        BadSignature { addr: String },
58        Signer(()),
59        Unknown,
60    }
61
62    #[derive(xmtp_common::ErrorCode)]
63    #[error_code(remote = "xmtp_cryptography::signature::IdentifierValidationError")]
64    enum IdentifierValidationError {
65        InvalidAddresses(Vec<String>),
66        HexDecode(()),
67        Generic(String),
68    }
69
70    #[derive(xmtp_common::ErrorCode)]
71    #[error_code(remote = "xmtp_cryptography::ethereum::EthereumCryptoError")]
72    enum EthereumCryptoError {
73        InvalidLength,
74        InvalidKey,
75        SignFailure,
76        DecompressFailure,
77    }
78}
79
80// Manual implementation for external hex crate error
81impl ErrorCode for hex::FromHexError {
82    fn error_code(&self) -> &'static str {
83        "hex::FromHexError"
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::ErrorCode;
90    use thiserror::Error;
91    use xmtp_macro::ErrorCode;
92
93    /// An inner error for testing.
94    #[derive(Debug, Error, ErrorCode)]
95    #[error("inner error")]
96    struct InnerError;
97
98    #[derive(Debug, Error, ErrorCode)]
99    enum StorageError {
100        /// Database connection failed.
101        #[error("connection failed")]
102        Connection,
103        /// Record not found.
104        #[error("not found")]
105        NotFound,
106    }
107
108    #[derive(Debug, Error, ErrorCode)]
109    enum GroupError {
110        /// Group not found in local database.
111        #[error("group not found")]
112        NotFound,
113        /// Group membership state is invalid.
114        #[error("invalid membership")]
115        InvalidMembership,
116        #[error("storage: {0}")]
117        #[error_code(inherit)]
118        Storage(#[from] StorageError),
119        #[error("inner: {0}")]
120        #[error_code(inherit)]
121        Inner(#[from] InnerError),
122    }
123
124    #[test]
125    fn test_struct_error_code() {
126        let err = InnerError;
127        assert_eq!(err.error_code(), "InnerError");
128    }
129
130    #[test]
131    fn test_enum_error_code() {
132        let err = StorageError::Connection;
133        assert_eq!(err.error_code(), "StorageError::Connection");
134
135        let err = StorageError::NotFound;
136        assert_eq!(err.error_code(), "StorageError::NotFound");
137    }
138
139    #[test]
140    fn test_inherited_error_code() {
141        let err = GroupError::NotFound;
142        assert_eq!(err.error_code(), "GroupError::NotFound");
143
144        let err = GroupError::InvalidMembership;
145        assert_eq!(err.error_code(), "GroupError::InvalidMembership");
146
147        // Inherited from StorageError
148        let err = GroupError::Storage(StorageError::Connection);
149        assert_eq!(err.error_code(), "StorageError::Connection");
150
151        // Inherited from InnerError (struct)
152        let err = GroupError::Inner(InnerError);
153        assert_eq!(err.error_code(), "InnerError");
154    }
155
156    #[test]
157    fn test_boxed_error_code() {
158        let err = Box::new(StorageError::Connection);
159        assert_eq!(err.error_code(), "StorageError::Connection");
160    }
161
162    #[test]
163    fn test_ref_error_code() {
164        let err = StorageError::Connection;
165        let err_ref = &err;
166        assert_eq!(err_ref.error_code(), "StorageError::Connection");
167    }
168
169    // Test custom code override for backwards compatibility
170    #[derive(Debug, Error, ErrorCode)]
171    enum RenamedError {
172        /// Variant was renamed but keeps old code for compatibility.
173        #[error("new name for the variant")]
174        #[error_code("RenamedError::OldVariantName")]
175        NewVariantName,
176        /// Another variant for testing.
177        #[error("another variant")]
178        AnotherVariant,
179    }
180
181    #[test]
182    fn test_custom_error_code() {
183        // Custom code preserves backwards compatibility
184        let err = RenamedError::NewVariantName;
185        assert_eq!(err.error_code(), "RenamedError::OldVariantName");
186
187        // Default code generation still works
188        let err = RenamedError::AnotherVariant;
189        assert_eq!(err.error_code(), "RenamedError::AnotherVariant");
190    }
191
192    // Tests for manual implementations of external types
193
194    #[test]
195    fn test_signature_error_codes() {
196        use xmtp_cryptography::signature::SignatureError;
197
198        // BadAddressFormat wraps hex::FromHexError
199        let err = SignatureError::BadAddressFormat(hex::FromHexError::OddLength);
200        assert_eq!(err.error_code(), "SignatureError::BadAddressFormat");
201
202        // BadSignature has an addr field
203        let err = SignatureError::BadSignature {
204            addr: "0x123".to_string(),
205        };
206        assert_eq!(err.error_code(), "SignatureError::BadSignature");
207
208        let err = SignatureError::Unknown;
209        assert_eq!(err.error_code(), "SignatureError::Unknown");
210    }
211
212    #[test]
213    fn test_identifier_validation_error_codes() {
214        use xmtp_cryptography::signature::IdentifierValidationError;
215
216        let err = IdentifierValidationError::InvalidAddresses(vec!["bad".to_string()]);
217        assert_eq!(
218            err.error_code(),
219            "IdentifierValidationError::InvalidAddresses"
220        );
221
222        let err = IdentifierValidationError::HexDecode(hex::FromHexError::OddLength);
223        assert_eq!(err.error_code(), "IdentifierValidationError::HexDecode");
224
225        let err = IdentifierValidationError::Generic("generic error".to_string());
226        assert_eq!(err.error_code(), "IdentifierValidationError::Generic");
227    }
228
229    #[test]
230    fn test_ethereum_crypto_error_codes() {
231        use xmtp_cryptography::ethereum::EthereumCryptoError;
232
233        let err = EthereumCryptoError::InvalidLength;
234        assert_eq!(err.error_code(), "EthereumCryptoError::InvalidLength");
235
236        let err = EthereumCryptoError::InvalidKey;
237        assert_eq!(err.error_code(), "EthereumCryptoError::InvalidKey");
238
239        let err = EthereumCryptoError::SignFailure;
240        assert_eq!(err.error_code(), "EthereumCryptoError::SignFailure");
241
242        let err = EthereumCryptoError::DecompressFailure;
243        assert_eq!(err.error_code(), "EthereumCryptoError::DecompressFailure");
244    }
245
246    #[test]
247    fn test_hex_from_hex_error_code() {
248        let err = hex::FromHexError::OddLength;
249        assert_eq!(err.error_code(), "hex::FromHexError");
250
251        let err = hex::FromHexError::InvalidHexCharacter { c: 'Z', index: 0 };
252        assert_eq!(err.error_code(), "hex::FromHexError");
253
254        let err = hex::FromHexError::InvalidStringLength;
255        assert_eq!(err.error_code(), "hex::FromHexError");
256    }
257}