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("identity_not_found")]
181 IdentityNotFound,
182
183 #[error("no_successful_capture_found")]
185 NoSuccessfulCaptureFound,
186
187 #[error("not_eligible_for_recovery")]
189 NotEligibleForRecovery,
190 #[error("ohttp_error: {error}")]
192 OhttpError {
193 error: String,
195 },
196
197 #[error("invalid_action_for_session")]
199 InvalidActionSession,
200}
201
202impl From<reqwest::Error> for WalletKitError {
203 fn from(error: reqwest::Error) -> Self {
204 Self::Reqwest {
205 error: error.to_string(),
206 }
207 }
208}
209
210impl From<PrimitiveError> for WalletKitError {
211 fn from(error: PrimitiveError) -> Self {
212 match error {
213 PrimitiveError::InvalidInput { attribute, reason } => {
214 Self::InvalidInput { attribute, reason }
215 }
216 PrimitiveError::Serialization(error) => Self::SerializationError { error },
217 PrimitiveError::Deserialization(reason) => Self::InvalidInput {
218 attribute: "deserialization".to_string(),
219 reason,
220 },
221 PrimitiveError::NotInField => Self::InvalidInput {
222 attribute: "field_element".to_string(),
223 reason: "Provided value is not in the field".to_string(),
224 },
225 PrimitiveError::OutOfBounds => Self::InvalidInput {
226 attribute: "index".to_string(),
227 reason: "Provided index is out of bounds".to_string(),
228 },
229 }
230 }
231}
232
233impl From<WorldIdRequestAuthError> for WalletKitError {
234 fn from(error: WorldIdRequestAuthError) -> Self {
235 match error {
236 WorldIdRequestAuthError::InvalidRpSignature => Self::InvalidRpSignature,
237 WorldIdRequestAuthError::DuplicateNonce => Self::DuplicateNonce,
238 WorldIdRequestAuthError::UnknownRp => Self::UnknownRp,
239 WorldIdRequestAuthError::InactiveRp => Self::InactiveRp,
240 WorldIdRequestAuthError::TimestampTooOld => Self::TimestampTooOld,
241 WorldIdRequestAuthError::TimestampTooFarInFuture => {
242 Self::TimestampTooFarInFuture
243 }
244 WorldIdRequestAuthError::InvalidTimestamp => Self::InvalidTimestamp,
245 WorldIdRequestAuthError::RpSignatureExpired => Self::RpSignatureExpired,
246 WorldIdRequestAuthError::InvalidActionSession => Self::InvalidActionSession,
247 _ => Self::ProofGeneration {
248 error: error.to_string(),
249 },
250 }
251 }
252}
253
254impl From<ProofError> for WalletKitError {
255 fn from(error: ProofError) -> Self {
256 match error {
257 ProofError::RequestAuthError(error) => Self::from(error),
258 _ => Self::ProofGeneration {
259 error: error.to_string(),
260 },
261 }
262 }
263}
264
265#[cfg(feature = "semaphore")]
266impl From<semaphore_rs::protocol::ProofError> for WalletKitError {
267 fn from(error: semaphore_rs::protocol::ProofError) -> Self {
268 Self::ProofGeneration {
269 error: error.to_string(),
270 }
271 }
272}
273
274impl From<StorageError> for WalletKitError {
275 fn from(error: StorageError) -> Self {
276 Self::Generic {
277 error: error.to_string(),
278 }
279 }
280}
281
282impl From<AuthenticatorError> for WalletKitError {
283 fn from(error: AuthenticatorError) -> Self {
284 match error {
285 AuthenticatorError::AccountDoesNotExist => Self::AccountDoesNotExist,
286
287 AuthenticatorError::NetworkError(error) => Self::NetworkError {
288 url: error
289 .url()
290 .map(std::string::ToString::to_string)
291 .unwrap_or_default(),
292 error: error.to_string(),
293 status: None,
294 },
295 AuthenticatorError::PublicKeyNotFound => Self::UnauthorizedAuthenticator,
296 AuthenticatorError::GatewayError { status, body } => Self::NetworkError {
297 url: "gateway".to_string(),
298 error: body,
299 status: Some(status.as_u16()),
300 },
301 AuthenticatorError::PrimitiveError(error) => Self::from(error),
302
303 AuthenticatorError::ProofError(error) => Self::from(error),
304
305 AuthenticatorError::IndexerError { status, body } => Self::NetworkError {
306 url: "indexer".to_string(),
307 error: body,
308 status: Some(status.as_u16()),
309 },
310 AuthenticatorError::UnfullfilableRequest => Self::UnfulfillableRequest,
311 AuthenticatorError::ResponseValidationError(err) => {
312 Self::ResponseValidation(err.to_string())
313 }
314 AuthenticatorError::SessionIdMismatch => Self::SessionIdMismatch,
315
316 AuthenticatorError::OhttpEncapsulationError(_)
317 | AuthenticatorError::BhttpError(_)
318 | AuthenticatorError::OhttpRelayError { .. }
319 | AuthenticatorError::InvalidServiceResponse(_) => Self::OhttpError {
320 error: error.to_string(),
321 },
322
323 _ => Self::AuthenticatorError {
324 error: error.to_string(),
325 },
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 fn walletkit_error_from_request_auth_error(
335 error: WorldIdRequestAuthError,
336 ) -> WalletKitError {
337 AuthenticatorError::ProofError(ProofError::RequestAuthError(error)).into()
338 }
339
340 fn promoted_error_code(error: &WalletKitError) -> Option<&'static str> {
341 match error {
342 WalletKitError::InvalidRpSignature => Some("invalid_rp_signature"),
343 WalletKitError::DuplicateNonce => Some("duplicate_nonce"),
344 WalletKitError::UnknownRp => Some("unknown_rp"),
345 WalletKitError::InactiveRp => Some("inactive_rp"),
346 WalletKitError::TimestampTooOld => Some("timestamp_too_old"),
347 WalletKitError::TimestampTooFarInFuture => {
348 Some("timestamp_too_far_in_future")
349 }
350 WalletKitError::InvalidTimestamp => Some("invalid_timestamp"),
351 WalletKitError::RpSignatureExpired => Some("rp_signature_expired"),
352 WalletKitError::InvalidActionSession => Some("invalid_action_for_session"),
353 _ => None,
354 }
355 }
356
357 #[test]
358 fn maps_rp_request_auth_errors_to_public_walletkit_errors() {
359 let cases = [
360 (
361 WorldIdRequestAuthError::InvalidRpSignature,
362 "invalid_rp_signature",
363 ),
364 (WorldIdRequestAuthError::DuplicateNonce, "duplicate_nonce"),
365 (WorldIdRequestAuthError::UnknownRp, "unknown_rp"),
366 (WorldIdRequestAuthError::InactiveRp, "inactive_rp"),
367 (
368 WorldIdRequestAuthError::TimestampTooOld,
369 "timestamp_too_old",
370 ),
371 (
372 WorldIdRequestAuthError::TimestampTooFarInFuture,
373 "timestamp_too_far_in_future",
374 ),
375 (
376 WorldIdRequestAuthError::InvalidTimestamp,
377 "invalid_timestamp",
378 ),
379 (
380 WorldIdRequestAuthError::RpSignatureExpired,
381 "rp_signature_expired",
382 ),
383 (
384 WorldIdRequestAuthError::InvalidActionSession,
385 "invalid_action_for_session",
386 ),
387 ];
388
389 for (request_auth_error, expected_code) in cases {
390 let error = walletkit_error_from_request_auth_error(request_auth_error);
391
392 assert_eq!(promoted_error_code(&error), Some(expected_code));
393 assert_eq!(error.to_string(), expected_code);
394 }
395 }
396
397 #[test]
398 fn keeps_non_promoted_request_auth_errors_as_proof_generation_errors() {
399 let error = walletkit_error_from_request_auth_error(
400 WorldIdRequestAuthError::InvalidMerkleRoot,
401 );
402
403 match error {
404 WalletKitError::ProofGeneration { error } => {
405 assert_eq!(error, "invalid_merkle_root");
406 }
407 other => panic!("expected proof generation error, got {other:?}"),
408 }
409 }
410}