Skip to main content

walletkit_core/
error.rs

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/// Error outputs from `WalletKit`
11#[derive(Debug, Error, uniffi::Error)]
12pub enum WalletKitError {
13    /// Invalid input provided (e.g., incorrect length, format, etc.)
14    #[error("invalid_input_{attribute}")]
15    InvalidInput {
16        /// The attribute that is invalid
17        attribute: String,
18        /// The reason the input is invalid
19        reason: String,
20    },
21
22    /// The presented data is not a valid U256 integer
23    #[error("invalid_number")]
24    InvalidNumber,
25
26    /// Unexpected error serializing information
27    #[error("serialization_error")]
28    SerializationError {
29        /// The error message from the serialization
30        error: String,
31    },
32
33    /// Network connection error with details
34    #[error("network_error at {url}: {error}")]
35    NetworkError {
36        /// The URL of the request
37        url: String,
38        /// The error message from the request
39        error: String,
40        /// The HTTP status code of the request, if available
41        status: Option<u16>,
42    },
43
44    /// HTTP request failure
45    #[error("request_error")]
46    Reqwest {
47        /// The error message from the request
48        error: String,
49    },
50
51    /// Unhandled error generating a Zero-Knowledge Proof
52    #[error("proof_generation_error: {error}")]
53    ProofGeneration {
54        /// The error message from the proof generation
55        error: String,
56    },
57
58    /// The `semaphore` feature flag is not enabled
59    #[error("semaphore_not_enabled")]
60    SemaphoreNotEnabled,
61
62    /// The requested credential is not issued for this World ID
63    #[error("credential_not_issued")]
64    CredentialNotIssued,
65
66    /// The requested credential has not been submitted on-chain
67    #[error("credential_not_mined")]
68    CredentialNotMined,
69
70    /// This operation requires a registered account and an account is not registered
71    /// for this authenticator. Call `create_account` first to register it.
72    #[error("Account is not registered for this authenticator.")]
73    AccountDoesNotExist,
74
75    /// The public key was not found in the batch, i.e. the authenticator is not authorized to sign for this action
76    #[error("unauthorized_authenticator")]
77    UnauthorizedAuthenticator,
78
79    /// An unexpected error occurred with the Authenticator
80    #[error("unexpected_authenticator_error: {error}")]
81    AuthenticatorError {
82        /// The error message from the authenticator
83        error: String,
84    },
85
86    /// The request could not be fulfilled with the credentials the user has available
87    #[error("unfulfillable_request")]
88    UnfulfillableRequest,
89
90    /// The response generated didn't match the request
91    ///
92    /// This occurs if the response doesn't match the requested proofs - e.g. by ids
93    /// or doesn't satisfy the contraints declared in the request
94    #[error("invalid response: {0}")]
95    ResponseValidation(String),
96
97    /// The generated nullifier has already been used in a proof submission and cannot be used again
98    #[error("nullifier_replay")]
99    NullifierReplay,
100
101    /// The RP's signature on the proof request could not be verified.
102    #[error("invalid_rp_signature")]
103    InvalidRpSignature,
104
105    /// The RP reused a signature nonce.
106    #[error("duplicate_nonce")]
107    DuplicateNonce,
108
109    /// The RP is unknown to the World ID registry.
110    #[error("unknown_rp")]
111    UnknownRp,
112
113    /// The RP is inactive and cannot request proofs.
114    #[error("inactive_rp")]
115    InactiveRp,
116
117    /// The RP's request timestamp is too old.
118    #[error("timestamp_too_old")]
119    TimestampTooOld,
120
121    /// The RP's request timestamp is too far in the future.
122    #[error("timestamp_too_far_in_future")]
123    TimestampTooFarInFuture,
124
125    /// The RP's request timestamp could not be parsed.
126    #[error("invalid_timestamp")]
127    InvalidTimestamp,
128
129    /// The RP's signature has expired.
130    #[error("rp_signature_expired")]
131    RpSignatureExpired,
132
133    /// Cached Groth16 material could not be parsed or verified.
134    #[error("groth16_material_cache_invalid")]
135    Groth16MaterialCacheInvalid {
136        /// Input path(s) used for loading.
137        path: String,
138        /// Underlying error message.
139        error: String,
140    },
141
142    /// Failed to load embedded Groth16 material.
143    #[error("groth16_material_embedded_load")]
144    Groth16MaterialEmbeddedLoad {
145        /// Underlying error message.
146        error: String,
147    },
148
149    /// An unexpected error occurred
150    #[error("unexpected_error: {error}")]
151    Generic {
152        /// The details of the error
153        error: String,
154    },
155
156    /// The recovery binding does not exist
157    #[error("recovery_binding_does_not_exist")]
158    RecoveryBindingDoesNotExist,
159
160    /// The session ID computed for this proof does not match the expected session ID from the proof request.
161    ///
162    /// This indicates the `session_id` provided by the RP is invalid or compromised, as
163    /// the only other failure option is OPRFs not having performed correct computations.
164    #[error("the expected session id and the generated session id do not match")]
165    SessionIdMismatch,
166
167    /// The NFC uniqueness service rejected the request with a permanent error
168    /// that will not resolve on retry (e.g. expired document).
169    #[error("nfc_non_retryable: {error_code}")]
170    NfcNonRetryable {
171        /// The error code from the NFC service (e.g. `document_expired`)
172        error_code: String,
173    },
174
175    /// The debug report was not found
176    #[error("debug_report_not_found")]
177    DebugReportNotFound,
178
179    /// The backend has no identity for the credential's `sub`
180    #[error("identity_not_found")]
181    IdentityNotFound,
182
183    /// The identity has no successful capture to check recovery eligibility against
184    #[error("no_successful_capture_found")]
185    NoSuccessfulCaptureFound,
186
187    /// The user is not eligible for recovery
188    #[error("not_eligible_for_recovery")]
189    NotEligibleForRecovery,
190    /// An error occurred in the OHTTP privacy layer (relay, encapsulation, or framing).
191    #[error("ohttp_error: {error}")]
192    OhttpError {
193        /// The error message from the OHTTP layer
194        error: String,
195    },
196
197    /// The session proof action or session OPRF seed is not valid for the session OPRF module.
198    #[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}