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