use r402_core::error::VerificationError;
use crate::chain::CasperChainReferenceFormatError;
use crate::motes::MotesParseError;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum CasperExactError {
#[error("unsupported scheme, expected exact")]
InvalidScheme,
#[error("network mismatch: payload={payload} requirements={requirements}")]
NetworkMismatch {
payload: String,
requirements: String,
},
#[error("accepted requirements do not match payment requirements")]
AcceptedRequirementsMismatch,
#[error("unsupported casper network: {0}")]
UnsupportedNetwork(#[from] CasperChainReferenceFormatError),
#[error("invalid asset (expected 64-char contract package hash): {0}")]
InvalidAsset(String),
#[error("invalid payTo address: {0}")]
InvalidPayTo(String),
#[error("invalid payer address: {0}")]
InvalidPayer(String),
#[error("payTo mismatch: authorization.to={authorization} requirements.payTo={requirements}")]
PayToMismatch {
authorization: String,
requirements: String,
},
#[error(
"amount mismatch: authorization.value={authorization} requirements.amount={requirements}"
)]
AmountMismatch {
authorization: String,
requirements: String,
},
#[error("payment amount must be non-zero")]
ZeroAmount,
#[error("invalid amount: {0}")]
InvalidAmount(#[from] MotesParseError),
#[error("authorization is not yet valid: validAfter={valid_after} now={now}")]
NotYetValid {
valid_after: u64,
now: u64,
},
#[error("authorization expired or too close to expiry: validBefore={valid_before} now={now}")]
Expired {
valid_before: u64,
now: u64,
},
#[error("invalid signature: {0}")]
InvalidSignature(String),
#[error("invalid nonce: {0}")]
InvalidNonce(String),
#[error("public key does not match authorization.from")]
PublicKeyMismatch,
#[error("requirements.extra.name is required for the CEP-18 EIP-712 domain")]
MissingTokenName,
#[error("requirements.extra.version is required for the CEP-18 EIP-712 domain")]
MissingTokenVersion,
}
impl From<CasperExactError> for VerificationError {
fn from(error: CasperExactError) -> Self {
match error {
CasperExactError::InvalidScheme => Self::UnsupportedScheme,
CasperExactError::NetworkMismatch { .. } => Self::ChainIdMismatch,
CasperExactError::AcceptedRequirementsMismatch => Self::AcceptedRequirementsMismatch,
CasperExactError::UnsupportedNetwork(_) => Self::UnsupportedChain,
CasperExactError::InvalidAsset(_) => Self::AssetMismatch,
CasperExactError::PayToMismatch { .. } => Self::RecipientMismatch,
CasperExactError::AmountMismatch { .. } | CasperExactError::ZeroAmount => {
Self::InvalidPaymentAmount
}
CasperExactError::NotYetValid { .. } => Self::Early,
CasperExactError::Expired { .. } => Self::Expired,
CasperExactError::InvalidSignature(_) | CasperExactError::PublicKeyMismatch => {
Self::InvalidSignature(error.to_string())
}
CasperExactError::InvalidPayTo(_)
| CasperExactError::InvalidPayer(_)
| CasperExactError::InvalidNonce(_)
| CasperExactError::InvalidAmount(_)
| CasperExactError::MissingTokenName
| CasperExactError::MissingTokenVersion => Self::InvalidFormat(error.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use r402_core::error_reason::{AsPaymentProblem, ErrorReason};
use super::*;
fn reason(error: CasperExactError) -> ErrorReason {
VerificationError::from(error).as_payment_problem().reason()
}
#[test]
fn scheme_and_network_errors_map_to_spec_reasons() {
assert_eq!(
reason(CasperExactError::InvalidScheme),
ErrorReason::UnsupportedScheme
);
assert_eq!(
reason(CasperExactError::NetworkMismatch {
payload: "casper:casper".to_owned(),
requirements: "casper:casper-test".to_owned(),
}),
ErrorReason::InvalidNetwork
);
assert_eq!(
reason(CasperExactError::AcceptedRequirementsMismatch),
ErrorReason::InvalidPaymentRequirements
);
}
#[test]
fn amount_errors_map_to_payment_requirements() {
assert_eq!(
reason(CasperExactError::ZeroAmount),
ErrorReason::InvalidPaymentRequirements
);
assert_eq!(
reason(CasperExactError::AmountMismatch {
authorization: "1".to_owned(),
requirements: "2".to_owned(),
}),
ErrorReason::InvalidPaymentRequirements
);
}
#[test]
fn timing_and_signature_errors_map_to_invalid_payload() {
assert_eq!(
reason(CasperExactError::Expired {
valid_before: 1,
now: 2
}),
ErrorReason::InvalidPayload
);
assert_eq!(
reason(CasperExactError::NotYetValid {
valid_after: 2,
now: 1
}),
ErrorReason::InvalidPayload
);
assert_eq!(
reason(CasperExactError::PublicKeyMismatch),
ErrorReason::InvalidPayload
);
}
#[test]
fn missing_domain_fields_map_to_invalid_payload() {
assert_eq!(
reason(CasperExactError::MissingTokenName),
ErrorReason::InvalidPayload
);
assert_eq!(
reason(CasperExactError::MissingTokenVersion),
ErrorReason::InvalidPayload
);
}
#[test]
fn sub_mote_precision_surfaces_as_invalid_format() {
let error = CasperExactError::from(MotesParseError::SubMotePrecision { digits: 10 });
let verification = VerificationError::from(error);
assert!(
verification.to_string().contains("fractional digits"),
"precision failures must not be silently swallowed: {verification}"
);
}
}