use thiserror::Error;
use world_id_core::{
primitives::{oprf::WorldIdRequestAuthError, PrimitiveError},
AuthenticatorError,
};
use world_id_proof::ProofError;
use crate::storage::StorageError;
#[derive(Debug, Error, uniffi::Error)]
pub enum WalletKitError {
#[error("invalid_input_{attribute}")]
InvalidInput {
attribute: String,
reason: String,
},
#[error("invalid_number")]
InvalidNumber,
#[error("serialization_error")]
SerializationError {
error: String,
},
#[error("network_error at {url}: {error}")]
NetworkError {
url: String,
error: String,
status: Option<u16>,
},
#[error("request_error")]
Reqwest {
error: String,
},
#[error("proof_generation_error: {error}")]
ProofGeneration {
error: String,
},
#[error("semaphore_not_enabled")]
SemaphoreNotEnabled,
#[error("credential_not_issued")]
CredentialNotIssued,
#[error("credential_not_mined")]
CredentialNotMined,
#[error("Account is not registered for this authenticator.")]
AccountDoesNotExist,
#[error("unauthorized_authenticator")]
UnauthorizedAuthenticator,
#[error("unexpected_authenticator_error: {error}")]
AuthenticatorError {
error: String,
},
#[error("unfulfillable_request")]
UnfulfillableRequest,
#[error("invalid response: {0}")]
ResponseValidation(String),
#[error("nullifier_replay")]
NullifierReplay,
#[error("invalid_rp_signature")]
InvalidRpSignature,
#[error("duplicate_nonce")]
DuplicateNonce,
#[error("unknown_rp")]
UnknownRp,
#[error("inactive_rp")]
InactiveRp,
#[error("timestamp_too_old")]
TimestampTooOld,
#[error("timestamp_too_far_in_future")]
TimestampTooFarInFuture,
#[error("invalid_timestamp")]
InvalidTimestamp,
#[error("rp_signature_expired")]
RpSignatureExpired,
#[error("groth16_material_cache_invalid")]
Groth16MaterialCacheInvalid {
path: String,
error: String,
},
#[error("groth16_material_embedded_load")]
Groth16MaterialEmbeddedLoad {
error: String,
},
#[error("unexpected_error: {error}")]
Generic {
error: String,
},
#[error("recovery_binding_does_not_exist")]
RecoveryBindingDoesNotExist,
#[error("the expected session id and the generated session id do not match")]
SessionIdMismatch,
#[error("nfc_non_retryable: {error_code}")]
NfcNonRetryable {
error_code: String,
},
#[error("debug_report_not_found")]
DebugReportNotFound,
#[error("not_eligible_for_recovery")]
NotEligibleForRecovery,
#[error("ohttp_error: {error}")]
OhttpError {
error: String,
},
}
impl From<reqwest::Error> for WalletKitError {
fn from(error: reqwest::Error) -> Self {
Self::Reqwest {
error: error.to_string(),
}
}
}
impl From<PrimitiveError> for WalletKitError {
fn from(error: PrimitiveError) -> Self {
match error {
PrimitiveError::InvalidInput { attribute, reason } => {
Self::InvalidInput { attribute, reason }
}
PrimitiveError::Serialization(error) => Self::SerializationError { error },
PrimitiveError::Deserialization(reason) => Self::InvalidInput {
attribute: "deserialization".to_string(),
reason,
},
PrimitiveError::NotInField => Self::InvalidInput {
attribute: "field_element".to_string(),
reason: "Provided value is not in the field".to_string(),
},
PrimitiveError::OutOfBounds => Self::InvalidInput {
attribute: "index".to_string(),
reason: "Provided index is out of bounds".to_string(),
},
}
}
}
impl From<WorldIdRequestAuthError> for WalletKitError {
fn from(error: WorldIdRequestAuthError) -> Self {
match error {
WorldIdRequestAuthError::InvalidRpSignature => Self::InvalidRpSignature,
WorldIdRequestAuthError::DuplicateNonce => Self::DuplicateNonce,
WorldIdRequestAuthError::UnknownRp => Self::UnknownRp,
WorldIdRequestAuthError::InactiveRp => Self::InactiveRp,
WorldIdRequestAuthError::TimestampTooOld => Self::TimestampTooOld,
WorldIdRequestAuthError::TimestampTooFarInFuture => {
Self::TimestampTooFarInFuture
}
WorldIdRequestAuthError::InvalidTimestamp => Self::InvalidTimestamp,
WorldIdRequestAuthError::RpSignatureExpired => Self::RpSignatureExpired,
_ => Self::ProofGeneration {
error: error.to_string(),
},
}
}
}
impl From<ProofError> for WalletKitError {
fn from(error: ProofError) -> Self {
match error {
ProofError::RequestAuthError(error) => Self::from(error),
_ => Self::ProofGeneration {
error: error.to_string(),
},
}
}
}
#[cfg(feature = "semaphore")]
impl From<semaphore_rs::protocol::ProofError> for WalletKitError {
fn from(error: semaphore_rs::protocol::ProofError) -> Self {
Self::ProofGeneration {
error: error.to_string(),
}
}
}
impl From<StorageError> for WalletKitError {
fn from(error: StorageError) -> Self {
Self::Generic {
error: error.to_string(),
}
}
}
impl From<AuthenticatorError> for WalletKitError {
fn from(error: AuthenticatorError) -> Self {
match error {
AuthenticatorError::AccountDoesNotExist => Self::AccountDoesNotExist,
AuthenticatorError::NetworkError(error) => Self::NetworkError {
url: error
.url()
.map(std::string::ToString::to_string)
.unwrap_or_default(),
error: error.to_string(),
status: None,
},
AuthenticatorError::PublicKeyNotFound => Self::UnauthorizedAuthenticator,
AuthenticatorError::GatewayError { status, body } => Self::NetworkError {
url: "gateway".to_string(),
error: body,
status: Some(status.as_u16()),
},
AuthenticatorError::PrimitiveError(error) => Self::from(error),
AuthenticatorError::ProofError(error) => Self::from(error),
AuthenticatorError::IndexerError { status, body } => Self::NetworkError {
url: "indexer".to_string(),
error: body,
status: Some(status.as_u16()),
},
AuthenticatorError::UnfullfilableRequest => Self::UnfulfillableRequest,
AuthenticatorError::ResponseValidationError(err) => {
Self::ResponseValidation(err.to_string())
}
AuthenticatorError::SessionIdMismatch => Self::SessionIdMismatch,
AuthenticatorError::OhttpEncapsulationError(_)
| AuthenticatorError::BhttpError(_)
| AuthenticatorError::OhttpRelayError { .. }
| AuthenticatorError::InvalidServiceResponse(_) => Self::OhttpError {
error: error.to_string(),
},
_ => Self::AuthenticatorError {
error: error.to_string(),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn walletkit_error_from_request_auth_error(
error: WorldIdRequestAuthError,
) -> WalletKitError {
AuthenticatorError::ProofError(ProofError::RequestAuthError(error)).into()
}
fn promoted_error_code(error: &WalletKitError) -> Option<&'static str> {
match error {
WalletKitError::InvalidRpSignature => Some("invalid_rp_signature"),
WalletKitError::DuplicateNonce => Some("duplicate_nonce"),
WalletKitError::UnknownRp => Some("unknown_rp"),
WalletKitError::InactiveRp => Some("inactive_rp"),
WalletKitError::TimestampTooOld => Some("timestamp_too_old"),
WalletKitError::TimestampTooFarInFuture => {
Some("timestamp_too_far_in_future")
}
WalletKitError::InvalidTimestamp => Some("invalid_timestamp"),
WalletKitError::RpSignatureExpired => Some("rp_signature_expired"),
_ => None,
}
}
#[test]
fn maps_rp_request_auth_errors_to_public_walletkit_errors() {
let cases = [
(
WorldIdRequestAuthError::InvalidRpSignature,
"invalid_rp_signature",
),
(WorldIdRequestAuthError::DuplicateNonce, "duplicate_nonce"),
(WorldIdRequestAuthError::UnknownRp, "unknown_rp"),
(WorldIdRequestAuthError::InactiveRp, "inactive_rp"),
(
WorldIdRequestAuthError::TimestampTooOld,
"timestamp_too_old",
),
(
WorldIdRequestAuthError::TimestampTooFarInFuture,
"timestamp_too_far_in_future",
),
(
WorldIdRequestAuthError::InvalidTimestamp,
"invalid_timestamp",
),
(
WorldIdRequestAuthError::RpSignatureExpired,
"rp_signature_expired",
),
];
for (request_auth_error, expected_code) in cases {
let error = walletkit_error_from_request_auth_error(request_auth_error);
assert_eq!(promoted_error_code(&error), Some(expected_code));
assert_eq!(error.to_string(), expected_code);
}
}
#[test]
fn keeps_non_promoted_request_auth_errors_as_proof_generation_errors() {
let error = walletkit_error_from_request_auth_error(
WorldIdRequestAuthError::InvalidMerkleRoot,
);
match error {
WalletKitError::ProofGeneration { error } => {
assert_eq!(error, "invalid_merkle_root");
}
other => panic!("expected proof generation error, got {other:?}"),
}
}
}