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