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 PrimitiveError::SessionIdCommitmentMismatch => Self::SessionIdMismatch,
230 }
231 }
232}
233
234impl From<WorldIdRequestAuthError> for WalletKitError {
235 fn from(error: WorldIdRequestAuthError) -> Self {
236 match error {
237 WorldIdRequestAuthError::InvalidRpSignature => Self::InvalidRpSignature,
238 WorldIdRequestAuthError::DuplicateNonce => Self::DuplicateNonce,
239 WorldIdRequestAuthError::UnknownRp => Self::UnknownRp,
240 WorldIdRequestAuthError::InactiveRp => Self::InactiveRp,
241 WorldIdRequestAuthError::CreatedAtTooOld => Self::TimestampTooOld,
242 WorldIdRequestAuthError::CreatedAtTooFarInFuture => {
243 Self::TimestampTooFarInFuture
244 }
245 WorldIdRequestAuthError::InvalidTimestamp => Self::InvalidTimestamp,
246 WorldIdRequestAuthError::RpSignatureExpired => Self::RpSignatureExpired,
247 WorldIdRequestAuthError::InvalidActionSession => Self::InvalidActionSession,
248 _ => Self::ProofGeneration {
249 error: error.to_string(),
250 },
251 }
252 }
253}
254
255impl From<ProofError> for WalletKitError {
256 fn from(error: ProofError) -> Self {
257 match error {
258 ProofError::RequestAuthError(error) => Self::from(error),
259 _ => Self::ProofGeneration {
260 error: error.to_string(),
261 },
262 }
263 }
264}
265
266#[cfg(feature = "semaphore")]
267impl From<semaphore_rs::protocol::ProofError> for WalletKitError {
268 fn from(error: semaphore_rs::protocol::ProofError) -> Self {
269 Self::ProofGeneration {
270 error: error.to_string(),
271 }
272 }
273}
274
275impl From<StorageError> for WalletKitError {
276 fn from(error: StorageError) -> Self {
277 Self::Generic {
278 error: error.to_string(),
279 }
280 }
281}
282
283impl From<AuthenticatorError> for WalletKitError {
284 fn from(error: AuthenticatorError) -> Self {
285 match error {
286 AuthenticatorError::AccountDoesNotExist => Self::AccountDoesNotExist,
287
288 AuthenticatorError::NetworkError(error) => Self::NetworkError {
289 url: error
290 .url()
291 .map(std::string::ToString::to_string)
292 .unwrap_or_default(),
293 error: error.to_string(),
294 status: None,
295 },
296 AuthenticatorError::PublicKeyNotFound => Self::UnauthorizedAuthenticator,
297 AuthenticatorError::GatewayError { status, body } => Self::NetworkError {
298 url: "gateway".to_string(),
299 error: body,
300 status: Some(status.as_u16()),
301 },
302 AuthenticatorError::PrimitiveError(error) => Self::from(error),
303
304 AuthenticatorError::ProofError(error) => Self::from(error),
305
306 AuthenticatorError::IndexerError { status, body } => Self::NetworkError {
307 url: "indexer".to_string(),
308 error: body,
309 status: Some(status.as_u16()),
310 },
311 AuthenticatorError::UnfullfilableRequest => Self::UnfulfillableRequest,
312 AuthenticatorError::ResponseValidationError(err) => {
313 Self::ResponseValidation(err.to_string())
314 }
315 AuthenticatorError::OhttpEncapsulationError(_)
316 | AuthenticatorError::BhttpError(_)
317 | AuthenticatorError::OhttpRelayError { .. }
318 | AuthenticatorError::InvalidServiceResponse(_) => Self::OhttpError {
319 error: error.to_string(),
320 },
321
322 _ => Self::AuthenticatorError {
323 error: error.to_string(),
324 },
325 }
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332
333 fn walletkit_error_from_request_auth_error(
334 error: WorldIdRequestAuthError,
335 ) -> WalletKitError {
336 AuthenticatorError::ProofError(ProofError::RequestAuthError(error)).into()
337 }
338
339 fn promoted_error_code(error: &WalletKitError) -> Option<&'static str> {
340 match error {
341 WalletKitError::InvalidRpSignature => Some("invalid_rp_signature"),
342 WalletKitError::DuplicateNonce => Some("duplicate_nonce"),
343 WalletKitError::UnknownRp => Some("unknown_rp"),
344 WalletKitError::InactiveRp => Some("inactive_rp"),
345 WalletKitError::TimestampTooOld => Some("timestamp_too_old"),
346 WalletKitError::TimestampTooFarInFuture => {
347 Some("timestamp_too_far_in_future")
348 }
349 WalletKitError::InvalidTimestamp => Some("invalid_timestamp"),
350 WalletKitError::RpSignatureExpired => Some("rp_signature_expired"),
351 WalletKitError::InvalidActionSession => Some("invalid_action_for_session"),
352 _ => None,
353 }
354 }
355
356 #[test]
357 fn maps_rp_request_auth_errors_to_public_walletkit_errors() {
358 let cases = [
359 (
360 WorldIdRequestAuthError::InvalidRpSignature,
361 "invalid_rp_signature",
362 ),
363 (WorldIdRequestAuthError::DuplicateNonce, "duplicate_nonce"),
364 (WorldIdRequestAuthError::UnknownRp, "unknown_rp"),
365 (WorldIdRequestAuthError::InactiveRp, "inactive_rp"),
366 (
367 WorldIdRequestAuthError::CreatedAtTooOld,
368 "timestamp_too_old",
369 ),
370 (
371 WorldIdRequestAuthError::CreatedAtTooFarInFuture,
372 "timestamp_too_far_in_future",
373 ),
374 (
375 WorldIdRequestAuthError::InvalidTimestamp,
376 "invalid_timestamp",
377 ),
378 (
379 WorldIdRequestAuthError::RpSignatureExpired,
380 "rp_signature_expired",
381 ),
382 (
383 WorldIdRequestAuthError::InvalidActionSession,
384 "invalid_action_for_session",
385 ),
386 ];
387
388 for (request_auth_error, expected_code) in cases {
389 let error = walletkit_error_from_request_auth_error(request_auth_error);
390
391 assert_eq!(promoted_error_code(&error), Some(expected_code));
392 assert_eq!(error.to_string(), expected_code);
393 }
394 }
395
396 #[test]
397 fn keeps_non_promoted_request_auth_errors_as_proof_generation_errors() {
398 let error = walletkit_error_from_request_auth_error(
399 WorldIdRequestAuthError::InvalidMerkleRoot,
400 );
401
402 match error {
403 WalletKitError::ProofGeneration { error } => {
404 assert_eq!(error, "invalid_merkle_root");
405 }
406 other => panic!("expected proof generation error, got {other:?}"),
407 }
408 }
409}