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::GeneratePostQuantumKeyError")]
54    enum GeneratePostQuantumKeyError {
55        Crypto(()),
56        Rand(()),
57    }
58
59    #[derive(xmtp_common::ErrorCode)]
60    #[error_code(remote = "xmtp_cryptography::signature::SignatureError")]
61    enum SignatureError {
62        BadAddressFormat(()),
63        BadSignatureFormat(()),
64        BadSignature { addr: String },
65        Signer(()),
66        Unknown,
67    }
68
69    #[derive(xmtp_common::ErrorCode)]
70    #[error_code(remote = "xmtp_cryptography::signature::IdentifierValidationError")]
71    enum IdentifierValidationError {
72        InvalidAddresses(Vec<String>),
73        HexDecode(()),
74        Generic(String),
75    }
76
77    #[derive(xmtp_common::ErrorCode)]
78    #[error_code(remote = "xmtp_cryptography::ethereum::EthereumCryptoError")]
79    enum EthereumCryptoError {
80        InvalidLength,
81        InvalidKey,
82        SignFailure,
83        DecompressFailure,
84    }
85}
86
87// Manual implementation for external hex crate error
88impl ErrorCode for hex::FromHexError {
89    fn error_code(&self) -> &'static str {
90        "hex::FromHexError"
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::ErrorCode;
97    use thiserror::Error;
98    use xmtp_macro::ErrorCode;
99
100    /// An inner error for testing.
101    #[derive(Debug, Error, ErrorCode)]
102    #[error("inner error")]
103    struct InnerError;
104
105    #[derive(Debug, Error, ErrorCode)]
106    enum StorageError {
107        /// Database connection failed.
108        #[error("connection failed")]
109        Connection,
110        /// Record not found.
111        #[error("not found")]
112        NotFound,
113    }
114
115    #[derive(Debug, Error, ErrorCode)]
116    enum GroupError {
117        /// Group not found in local database.
118        #[error("group not found")]
119        NotFound,
120        /// Group membership state is invalid.
121        #[error("invalid membership")]
122        InvalidMembership,
123        #[error("storage: {0}")]
124        #[error_code(inherit)]
125        Storage(#[from] StorageError),
126        #[error("inner: {0}")]
127        #[error_code(inherit)]
128        Inner(#[from] InnerError),
129    }
130
131    #[test]
132    fn test_struct_error_code() {
133        let err = InnerError;
134        assert_eq!(err.error_code(), "InnerError");
135    }
136
137    #[test]
138    fn test_enum_error_code() {
139        let err = StorageError::Connection;
140        assert_eq!(err.error_code(), "StorageError::Connection");
141
142        let err = StorageError::NotFound;
143        assert_eq!(err.error_code(), "StorageError::NotFound");
144    }
145
146    #[test]
147    fn test_inherited_error_code() {
148        let err = GroupError::NotFound;
149        assert_eq!(err.error_code(), "GroupError::NotFound");
150
151        let err = GroupError::InvalidMembership;
152        assert_eq!(err.error_code(), "GroupError::InvalidMembership");
153
154        // Inherited from StorageError
155        let err = GroupError::Storage(StorageError::Connection);
156        assert_eq!(err.error_code(), "StorageError::Connection");
157
158        // Inherited from InnerError (struct)
159        let err = GroupError::Inner(InnerError);
160        assert_eq!(err.error_code(), "InnerError");
161    }
162
163    #[test]
164    fn test_boxed_error_code() {
165        let err = Box::new(StorageError::Connection);
166        assert_eq!(err.error_code(), "StorageError::Connection");
167    }
168
169    #[test]
170    fn test_ref_error_code() {
171        let err = StorageError::Connection;
172        let err_ref = &err;
173        assert_eq!(err_ref.error_code(), "StorageError::Connection");
174    }
175
176    // Test custom code override for backwards compatibility
177    #[derive(Debug, Error, ErrorCode)]
178    enum RenamedError {
179        /// Variant was renamed but keeps old code for compatibility.
180        #[error("new name for the variant")]
181        #[error_code("RenamedError::OldVariantName")]
182        NewVariantName,
183        /// Another variant for testing.
184        #[error("another variant")]
185        AnotherVariant,
186    }
187
188    #[test]
189    fn test_custom_error_code() {
190        // Custom code preserves backwards compatibility
191        let err = RenamedError::NewVariantName;
192        assert_eq!(err.error_code(), "RenamedError::OldVariantName");
193
194        // Default code generation still works
195        let err = RenamedError::AnotherVariant;
196        assert_eq!(err.error_code(), "RenamedError::AnotherVariant");
197    }
198
199    // Tests for manual implementations of external types
200
201    #[test]
202    fn test_signature_error_codes() {
203        use xmtp_cryptography::signature::SignatureError;
204
205        // BadAddressFormat wraps hex::FromHexError
206        let err = SignatureError::BadAddressFormat(hex::FromHexError::OddLength);
207        assert_eq!(err.error_code(), "SignatureError::BadAddressFormat");
208
209        // BadSignature has an addr field
210        let err = SignatureError::BadSignature {
211            addr: "0x123".to_string(),
212        };
213        assert_eq!(err.error_code(), "SignatureError::BadSignature");
214
215        let err = SignatureError::Unknown;
216        assert_eq!(err.error_code(), "SignatureError::Unknown");
217    }
218
219    #[test]
220    fn test_identifier_validation_error_codes() {
221        use xmtp_cryptography::signature::IdentifierValidationError;
222
223        let err = IdentifierValidationError::InvalidAddresses(vec!["bad".to_string()]);
224        assert_eq!(
225            err.error_code(),
226            "IdentifierValidationError::InvalidAddresses"
227        );
228
229        let err = IdentifierValidationError::HexDecode(hex::FromHexError::OddLength);
230        assert_eq!(err.error_code(), "IdentifierValidationError::HexDecode");
231
232        let err = IdentifierValidationError::Generic("generic error".to_string());
233        assert_eq!(err.error_code(), "IdentifierValidationError::Generic");
234    }
235
236    #[test]
237    fn test_ethereum_crypto_error_codes() {
238        use xmtp_cryptography::ethereum::EthereumCryptoError;
239
240        let err = EthereumCryptoError::InvalidLength;
241        assert_eq!(err.error_code(), "EthereumCryptoError::InvalidLength");
242
243        let err = EthereumCryptoError::InvalidKey;
244        assert_eq!(err.error_code(), "EthereumCryptoError::InvalidKey");
245
246        let err = EthereumCryptoError::SignFailure;
247        assert_eq!(err.error_code(), "EthereumCryptoError::SignFailure");
248
249        let err = EthereumCryptoError::DecompressFailure;
250        assert_eq!(err.error_code(), "EthereumCryptoError::DecompressFailure");
251    }
252
253    #[test]
254    fn test_hex_from_hex_error_code() {
255        let err = hex::FromHexError::OddLength;
256        assert_eq!(err.error_code(), "hex::FromHexError");
257
258        let err = hex::FromHexError::InvalidHexCharacter { c: 'Z', index: 0 };
259        assert_eq!(err.error_code(), "hex::FromHexError");
260
261        let err = hex::FromHexError::InvalidStringLength;
262        assert_eq!(err.error_code(), "hex::FromHexError");
263    }
264}