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        }
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}