Skip to main content

ows_core/
error.rs

1use serde::{Serialize, Serializer};
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
4#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
5pub enum OwsErrorCode {
6    WalletNotFound,
7    ChainNotSupported,
8    InvalidPassphrase,
9    InvalidInput,
10    CaipParseError,
11    PolicyDenied,
12    ApiKeyNotFound,
13    ApiKeyExpired,
14}
15
16#[derive(Debug, Clone, thiserror::Error)]
17pub enum OwsError {
18    #[error("wallet not found: {id}")]
19    WalletNotFound { id: String },
20
21    #[error("chain not supported: {chain}")]
22    ChainNotSupported { chain: String },
23
24    #[error("invalid passphrase")]
25    InvalidPassphrase,
26
27    #[error("invalid input: {message}")]
28    InvalidInput { message: String },
29
30    #[error("CAIP parse error: {message}")]
31    CaipParseError { message: String },
32
33    #[error("policy denied: {reason}")]
34    PolicyDenied { policy_id: String, reason: String },
35
36    #[error("API key not found")]
37    ApiKeyNotFound,
38
39    #[error("API key expired: {id}")]
40    ApiKeyExpired { id: String },
41}
42
43impl OwsError {
44    pub fn code(&self) -> OwsErrorCode {
45        match self {
46            OwsError::WalletNotFound { .. } => OwsErrorCode::WalletNotFound,
47            OwsError::ChainNotSupported { .. } => OwsErrorCode::ChainNotSupported,
48            OwsError::InvalidPassphrase => OwsErrorCode::InvalidPassphrase,
49            OwsError::InvalidInput { .. } => OwsErrorCode::InvalidInput,
50            OwsError::CaipParseError { .. } => OwsErrorCode::CaipParseError,
51            OwsError::PolicyDenied { .. } => OwsErrorCode::PolicyDenied,
52            OwsError::ApiKeyNotFound => OwsErrorCode::ApiKeyNotFound,
53            OwsError::ApiKeyExpired { .. } => OwsErrorCode::ApiKeyExpired,
54        }
55    }
56}
57
58#[derive(Serialize)]
59struct ErrorPayload {
60    code: OwsErrorCode,
61    message: String,
62}
63
64impl Serialize for OwsError {
65    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
66        let payload = ErrorPayload {
67            code: self.code(),
68            message: self.to_string(),
69        };
70        payload.serialize(serializer)
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn test_code_mapping_wallet_not_found() {
80        let err = OwsError::WalletNotFound {
81            id: "abc".to_string(),
82        };
83        assert_eq!(err.code(), OwsErrorCode::WalletNotFound);
84    }
85
86    #[test]
87    fn test_code_mapping_all_variants() {
88        assert_eq!(
89            OwsError::ChainNotSupported { chain: "x".into() }.code(),
90            OwsErrorCode::ChainNotSupported
91        );
92        assert_eq!(
93            OwsError::InvalidPassphrase.code(),
94            OwsErrorCode::InvalidPassphrase
95        );
96        assert_eq!(
97            OwsError::InvalidInput {
98                message: "x".into()
99            }
100            .code(),
101            OwsErrorCode::InvalidInput
102        );
103        assert_eq!(
104            OwsError::CaipParseError {
105                message: "x".into()
106            }
107            .code(),
108            OwsErrorCode::CaipParseError
109        );
110        assert_eq!(
111            OwsError::PolicyDenied {
112                policy_id: "x".into(),
113                reason: "x".into()
114            }
115            .code(),
116            OwsErrorCode::PolicyDenied
117        );
118        assert_eq!(
119            OwsError::ApiKeyNotFound.code(),
120            OwsErrorCode::ApiKeyNotFound
121        );
122        assert_eq!(
123            OwsError::ApiKeyExpired { id: "x".into() }.code(),
124            OwsErrorCode::ApiKeyExpired
125        );
126    }
127
128    #[test]
129    fn test_display_output() {
130        let err = OwsError::WalletNotFound {
131            id: "abc-123".to_string(),
132        };
133        assert_eq!(err.to_string(), "wallet not found: abc-123");
134    }
135
136    #[test]
137    fn test_json_serialization_shape() {
138        let err = OwsError::WalletNotFound {
139            id: "abc-123".to_string(),
140        };
141        let json = serde_json::to_value(&err).unwrap();
142        assert_eq!(json["code"], "WALLET_NOT_FOUND");
143        assert_eq!(json["message"], "wallet not found: abc-123");
144    }
145
146    #[test]
147    fn test_caip_parse_error_serialization() {
148        let err = OwsError::CaipParseError {
149            message: "bad format".to_string(),
150        };
151        let json = serde_json::to_value(&err).unwrap();
152        assert_eq!(json["code"], "CAIP_PARSE_ERROR");
153        assert!(json["message"].as_str().unwrap().contains("bad format"));
154    }
155
156    #[test]
157    fn test_policy_denied_serialization() {
158        let err = OwsError::PolicyDenied {
159            policy_id: "spending-limit".into(),
160            reason: "exceeded daily limit".into(),
161        };
162        let json = serde_json::to_value(&err).unwrap();
163        assert_eq!(json["code"], "POLICY_DENIED");
164        assert!(json["message"]
165            .as_str()
166            .unwrap()
167            .contains("exceeded daily limit"));
168    }
169
170    #[test]
171    fn test_api_key_not_found_serialization() {
172        let err = OwsError::ApiKeyNotFound;
173        let json = serde_json::to_value(&err).unwrap();
174        assert_eq!(json["code"], "API_KEY_NOT_FOUND");
175    }
176
177    #[test]
178    fn test_api_key_expired_serialization() {
179        let err = OwsError::ApiKeyExpired {
180            id: "key-123".into(),
181        };
182        let json = serde_json::to_value(&err).unwrap();
183        assert_eq!(json["code"], "API_KEY_EXPIRED");
184        assert!(json["message"].as_str().unwrap().contains("key-123"));
185    }
186}