1use thiserror::Error;
2use world_id_core::{
3 primitives::{oprf::WorldIdRequestAuthError, PrimitiveError},
4 AuthenticatorError,
5};
6use world_id_proof::ProofError;
7
8use crate::storage::StorageError;
9
10#[derive(Debug, Error, uniffi::Error)]
12pub enum WalletKitError {
13 #[error("invalid_input_{attribute}")]
15 InvalidInput {
16 attribute: String,
18 reason: String,
20 },
21
22 #[error("invalid_number")]
24 InvalidNumber,
25
26 #[error("serialization_error")]
28 SerializationError {
29 error: String,
31 },
32
33 #[error("network_error at {url}: {error}")]
35 NetworkError {
36 url: String,
38 error: String,
40 status: Option<u16>,
42 },
43
44 #[error("request_error")]
46 Reqwest {
47 error: String,
49 },
50
51 #[error("proof_generation_error: {error}")]
53 ProofGeneration {
54 error: String,
56 },
57
58 #[error("semaphore_not_enabled")]
60 SemaphoreNotEnabled,
61
62 #[error("credential_not_issued")]
64 CredentialNotIssued,
65
66 #[error("credential_not_mined")]
68 CredentialNotMined,
69
70 #[error("Account is not registered for this authenticator.")]
73 AccountDoesNotExist,
74
75 #[error("unauthorized_authenticator")]
77 UnauthorizedAuthenticator,
78
79 #[error("unexpected_authenticator_error: {error}")]
81 AuthenticatorError {
82 error: String,
84 },
85
86 #[error("unfulfillable_request")]
88 UnfulfillableRequest,
89
90 #[error("invalid response: {0}")]
95 ResponseValidation(String),
96
97 #[error("nullifier_replay")]
99 NullifierReplay,
100
101 #[error("invalid_rp_signature")]
103 InvalidRpSignature,
104
105 #[error("duplicate_nonce")]
107 DuplicateNonce,
108
109 #[error("unknown_rp")]
111 UnknownRp,
112
113 #[error("inactive_rp")]
115 InactiveRp,
116
117 #[error("timestamp_too_old")]
119 TimestampTooOld,
120
121 #[error("timestamp_too_far_in_future")]
123 TimestampTooFarInFuture,
124
125 #[error("invalid_timestamp")]
127 InvalidTimestamp,
128
129 #[error("rp_signature_expired")]
131 RpSignatureExpired,
132
133 #[error("groth16_material_cache_invalid")]
135 Groth16MaterialCacheInvalid {
136 path: String,
138 error: String,
140 },
141
142 #[error("groth16_material_embedded_load")]
144 Groth16MaterialEmbeddedLoad {
145 error: String,
147 },
148
149 #[error("unexpected_error: {error}")]
151 Generic {
152 error: String,
154 },
155
156 #[error("recovery_binding_does_not_exist")]
158 RecoveryBindingDoesNotExist,
159
160 #[error("the expected session id and the generated session id do not match")]
165 SessionIdMismatch,
166
167 #[error("nfc_non_retryable: {error_code}")]
170 NfcNonRetryable {
171 error_code: String,
173 },
174
175 #[error("debug_report_not_found")]
177 DebugReportNotFound,
178
179 #[error("not_eligible_for_recovery")]
181 NotEligibleForRecovery,
182 #[error("ohttp_error: {error}")]
184 OhttpError {
185 error: String,
187 },
188
189 #[error("invalid_action_for_session")]
191 InvalidActionSession,
192}
193
194impl From<reqwest::Error> for WalletKitError {
195 fn from(error: reqwest::Error) -> Self {
196 Self::Reqwest {
197 error: error.to_string(),
198 }
199 }
200}
201
202impl From<PrimitiveError> for WalletKitError {
203 fn from(error: PrimitiveError) -> Self {
204 match error {
205 PrimitiveError::InvalidInput { attribute, reason } => {
206 Self::InvalidInput { attribute, reason }
207 }
208 PrimitiveError::Serialization(error) => Self::SerializationError { error },
209 PrimitiveError::Deserialization(reason) => Self::InvalidInput {
210 attribute: "deserialization".to_string(),
211 reason,
212 },
213 PrimitiveError::NotInField => Self::InvalidInput {
214 attribute: "field_element".to_string(),
215 reason: "Provided value is not in the field".to_string(),
216 },
217 PrimitiveError::OutOfBounds => Self::InvalidInput {
218 attribute: "index".to_string(),
219 reason: "Provided index is out of bounds".to_string(),
220 },
221 }
222 }
223}
224
225impl From<WorldIdRequestAuthError> for WalletKitError {
226 fn from(error: WorldIdRequestAuthError) -> Self {
227 match error {
228 WorldIdRequestAuthError::InvalidRpSignature => Self::InvalidRpSignature,
229 WorldIdRequestAuthError::DuplicateNonce => Self::DuplicateNonce,
230 WorldIdRequestAuthError::UnknownRp => Self::UnknownRp,
231 WorldIdRequestAuthError::InactiveRp => Self::InactiveRp,
232 WorldIdRequestAuthError::TimestampTooOld => Self::TimestampTooOld,
233 WorldIdRequestAuthError::TimestampTooFarInFuture => {
234 Self::TimestampTooFarInFuture
235 }
236 WorldIdRequestAuthError::InvalidTimestamp => Self::InvalidTimestamp,
237 WorldIdRequestAuthError::RpSignatureExpired => Self::RpSignatureExpired,
238 WorldIdRequestAuthError::InvalidActionSession => Self::InvalidActionSession,
239 _ => Self::ProofGeneration {
240 error: error.to_string(),
241 },
242 }
243 }
244}
245
246impl From<ProofError> for WalletKitError {
247 fn from(error: ProofError) -> Self {
248 match error {
249 ProofError::RequestAuthError(error) => Self::from(error),
250 _ => Self::ProofGeneration {
251 error: error.to_string(),
252 },
253 }
254 }
255}
256
257#[cfg(feature = "semaphore")]
258impl From<semaphore_rs::protocol::ProofError> for WalletKitError {
259 fn from(error: semaphore_rs::protocol::ProofError) -> Self {
260 Self::ProofGeneration {
261 error: error.to_string(),
262 }
263 }
264}
265
266impl From<StorageError> for WalletKitError {
267 fn from(error: StorageError) -> Self {
268 Self::Generic {
269 error: error.to_string(),
270 }
271 }
272}
273
274impl From<AuthenticatorError> for WalletKitError {
275 fn from(error: AuthenticatorError) -> Self {
276 match error {
277 AuthenticatorError::AccountDoesNotExist => Self::AccountDoesNotExist,
278
279 AuthenticatorError::NetworkError(error) => Self::NetworkError {
280 url: error
281 .url()
282 .map(std::string::ToString::to_string)
283 .unwrap_or_default(),
284 error: error.to_string(),
285 status: None,
286 },
287 AuthenticatorError::PublicKeyNotFound => Self::UnauthorizedAuthenticator,
288 AuthenticatorError::GatewayError { status, body } => Self::NetworkError {
289 url: "gateway".to_string(),
290 error: body,
291 status: Some(status.as_u16()),
292 },
293 AuthenticatorError::PrimitiveError(error) => Self::from(error),
294
295 AuthenticatorError::ProofError(error) => Self::from(error),
296
297 AuthenticatorError::IndexerError { status, body } => Self::NetworkError {
298 url: "indexer".to_string(),
299 error: body,
300 status: Some(status.as_u16()),
301 },
302 AuthenticatorError::UnfullfilableRequest => Self::UnfulfillableRequest,
303 AuthenticatorError::ResponseValidationError(err) => {
304 Self::ResponseValidation(err.to_string())
305 }
306 AuthenticatorError::SessionIdMismatch => Self::SessionIdMismatch,
307
308 AuthenticatorError::OhttpEncapsulationError(_)
309 | AuthenticatorError::BhttpError(_)
310 | AuthenticatorError::OhttpRelayError { .. }
311 | AuthenticatorError::InvalidServiceResponse(_) => Self::OhttpError {
312 error: error.to_string(),
313 },
314
315 _ => Self::AuthenticatorError {
316 error: error.to_string(),
317 },
318 }
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 fn walletkit_error_from_request_auth_error(
327 error: WorldIdRequestAuthError,
328 ) -> WalletKitError {
329 AuthenticatorError::ProofError(ProofError::RequestAuthError(error)).into()
330 }
331
332 fn promoted_error_code(error: &WalletKitError) -> Option<&'static str> {
333 match error {
334 WalletKitError::InvalidRpSignature => Some("invalid_rp_signature"),
335 WalletKitError::DuplicateNonce => Some("duplicate_nonce"),
336 WalletKitError::UnknownRp => Some("unknown_rp"),
337 WalletKitError::InactiveRp => Some("inactive_rp"),
338 WalletKitError::TimestampTooOld => Some("timestamp_too_old"),
339 WalletKitError::TimestampTooFarInFuture => {
340 Some("timestamp_too_far_in_future")
341 }
342 WalletKitError::InvalidTimestamp => Some("invalid_timestamp"),
343 WalletKitError::RpSignatureExpired => Some("rp_signature_expired"),
344 WalletKitError::InvalidActionSession => Some("invalid_action_for_session"),
345 _ => None,
346 }
347 }
348
349 #[test]
350 fn maps_rp_request_auth_errors_to_public_walletkit_errors() {
351 let cases = [
352 (
353 WorldIdRequestAuthError::InvalidRpSignature,
354 "invalid_rp_signature",
355 ),
356 (WorldIdRequestAuthError::DuplicateNonce, "duplicate_nonce"),
357 (WorldIdRequestAuthError::UnknownRp, "unknown_rp"),
358 (WorldIdRequestAuthError::InactiveRp, "inactive_rp"),
359 (
360 WorldIdRequestAuthError::TimestampTooOld,
361 "timestamp_too_old",
362 ),
363 (
364 WorldIdRequestAuthError::TimestampTooFarInFuture,
365 "timestamp_too_far_in_future",
366 ),
367 (
368 WorldIdRequestAuthError::InvalidTimestamp,
369 "invalid_timestamp",
370 ),
371 (
372 WorldIdRequestAuthError::RpSignatureExpired,
373 "rp_signature_expired",
374 ),
375 (
376 WorldIdRequestAuthError::InvalidActionSession,
377 "invalid_action_for_session",
378 ),
379 ];
380
381 for (request_auth_error, expected_code) in cases {
382 let error = walletkit_error_from_request_auth_error(request_auth_error);
383
384 assert_eq!(promoted_error_code(&error), Some(expected_code));
385 assert_eq!(error.to_string(), expected_code);
386 }
387 }
388
389 #[test]
390 fn keeps_non_promoted_request_auth_errors_as_proof_generation_errors() {
391 let error = walletkit_error_from_request_auth_error(
392 WorldIdRequestAuthError::InvalidMerkleRoot,
393 );
394
395 match error {
396 WalletKitError::ProofGeneration { error } => {
397 assert_eq!(error, "invalid_merkle_root");
398 }
399 other => panic!("expected proof generation error, got {other:?}"),
400 }
401 }
402}