use std::fmt::{self, Display, Formatter};
use compact_str::CompactString;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum VerificationError {
#[error("invalid format: {0}")]
InvalidFormat(String),
#[error("payment amount is below requirements")]
InvalidPaymentAmount,
#[error("payment authorization is not yet valid")]
Early,
#[error("payment authorization has expired")]
Expired,
#[error("chain id mismatch")]
ChainIdMismatch,
#[error("payment recipient mismatch")]
RecipientMismatch,
#[error("payment asset mismatch")]
AssetMismatch,
#[error("insufficient on-chain balance")]
InsufficientFunds,
#[error("permit2 allowance required")]
Permit2AllowanceRequired,
#[error("invalid signature: {0}")]
InvalidSignature(String),
#[error("simulation failed: {0}")]
SimulationFailed(String),
#[error("unsupported chain")]
UnsupportedChain,
#[error("unsupported scheme")]
UnsupportedScheme,
#[error("accepted details do not match requirements")]
AcceptedRequirementsMismatch,
#[error("authorization nonce already used")]
NonceAlreadyUsed,
#[error("duplicate settlement attempt")]
DuplicateSettlement,
#[error("memo data mismatch")]
MemoMismatch,
#[error("memo instruction count invalid (expected 1, got {count})")]
MemoInstructionCountInvalid {
count: usize,
},
#[error("settlement amount {requested} exceeds authorised maximum {authorised}")]
SettlementAmountExceedsPermitted {
requested: String,
authorised: String,
},
#[error("witness.facilitator {witness} is not authorised on this facilitator")]
UptoFacilitatorMismatch {
witness: String,
},
#[error("on-chain proxy rejected settle: msg.sender does not match witness.facilitator")]
UptoUnauthorizedFacilitator,
#[error("on-chain proxy rejected settle: amount exceeds permitted maximum")]
UptoAmountExceedsPermitted,
}
impl From<serde_json::Error> for VerificationError {
fn from(err: serde_json::Error) -> Self {
Self::InvalidFormat(err.to_string())
}
}
impl AsPaymentProblem for VerificationError {
fn as_payment_problem(&self) -> PaymentProblem {
let reason = match self {
Self::InvalidFormat(_) | Self::InvalidSignature(_) | Self::Early | Self::Expired => {
ErrorReason::InvalidPayload
}
Self::InvalidPaymentAmount
| Self::RecipientMismatch
| Self::AssetMismatch
| Self::AcceptedRequirementsMismatch => ErrorReason::InvalidPaymentRequirements,
Self::ChainIdMismatch | Self::UnsupportedChain => ErrorReason::InvalidNetwork,
Self::InsufficientFunds => ErrorReason::InsufficientFunds,
Self::Permit2AllowanceRequired => ErrorReason::Permit2AllowanceRequired,
Self::SimulationFailed(_) => ErrorReason::InvalidTransactionState,
Self::UnsupportedScheme => ErrorReason::UnsupportedScheme,
Self::NonceAlreadyUsed => ErrorReason::NonceAlreadyUsed,
Self::DuplicateSettlement => ErrorReason::DuplicateSettlement,
Self::MemoMismatch => ErrorReason::InvalidExactSolanaPayloadMemoMismatch,
Self::MemoInstructionCountInvalid { .. } => {
ErrorReason::InvalidExactSolanaPayloadMemoCount
}
Self::SettlementAmountExceedsPermitted { .. } => {
ErrorReason::InvalidUptoEvmPayloadSettlementExceedsAmount
}
Self::UptoFacilitatorMismatch { .. } => ErrorReason::UptoFacilitatorMismatch,
Self::UptoUnauthorizedFacilitator => ErrorReason::UptoUnauthorizedFacilitator,
Self::UptoAmountExceedsPermitted => ErrorReason::UptoAmountExceedsPermitted,
};
PaymentProblem::new(reason, self.to_string())
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SettlementError {
#[error("on-chain settlement failed: {0}")]
Onchain(String),
#[error("settlement timed out")]
Timeout,
#[error("duplicate settlement attempt")]
Duplicate,
}
impl AsPaymentProblem for SettlementError {
fn as_payment_problem(&self) -> PaymentProblem {
let reason = match self {
Self::Onchain(_) => ErrorReason::InvalidTransactionState,
Self::Timeout => ErrorReason::UnexpectedSettleError,
Self::Duplicate => ErrorReason::DuplicateSettlement,
};
PaymentProblem::new(reason, self.to_string())
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum FacilitatorError {
#[error(transparent)]
Verification(#[from] VerificationError),
#[error(transparent)]
Settlement(#[from] SettlementError),
#[error("{reason}: {message}")]
Aborted {
reason: String,
message: String,
},
#[error("on-chain error: {0}")]
Onchain(String),
#[error(transparent)]
Internal(Box<dyn std::error::Error + Send + Sync>),
}
impl FacilitatorError {
#[must_use]
pub fn aborted(reason: impl Into<String>, message: impl Into<String>) -> Self {
Self::Aborted {
reason: reason.into(),
message: message.into(),
}
}
#[must_use]
pub fn internal<E>(err: E) -> Self
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
Self::Internal(err.into())
}
}
impl AsPaymentProblem for FacilitatorError {
fn as_payment_problem(&self) -> PaymentProblem {
match self {
Self::Verification(e) => e.as_payment_problem(),
Self::Settlement(e) => e.as_payment_problem(),
Self::Aborted { reason, message } => PaymentProblem::new(
ErrorReason::from_wire(reason),
format!("{reason}: {message}"),
),
Self::Onchain(message) => {
PaymentProblem::new(ErrorReason::InvalidTransactionState, message.clone())
}
Self::Internal(e) => {
PaymentProblem::new(ErrorReason::UnexpectedVerifyError, e.to_string())
}
}
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ClientError {
#[error("no matching payment option")]
NoMatchingPaymentOption,
#[error("request is not cloneable (streaming body?)")]
RequestNotCloneable,
#[error("failed to parse 402 response: {0}")]
Parse(String),
#[error("failed to sign payment: {0}")]
Signing(String),
#[error("payment pre-condition not met: {0}")]
PreConditionFailed(String),
#[error(transparent)]
Json(#[from] serde_json::Error),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ErrorReason {
InsufficientFunds,
InvalidExactEvmPayloadAuthorizationValidAfter,
InvalidExactEvmPayloadAuthorizationValidBefore,
InvalidExactEvmPayloadAuthorizationValueMismatch,
InvalidExactEvmPayloadSignature,
InvalidExactEvmPayloadRecipientMismatch,
InvalidNetwork,
InvalidPayload,
InvalidPaymentRequirements,
InvalidScheme,
UnsupportedScheme,
InvalidX402Version,
InvalidTransactionState,
UnexpectedVerifyError,
UnexpectedSettleError,
DuplicateSettlement,
NonceAlreadyUsed,
Permit2AllowanceRequired,
InvalidUptoEvmPayloadSettlementExceedsAmount,
UptoFacilitatorMismatch,
UptoUnauthorizedFacilitator,
UptoAmountExceedsPermitted,
InvalidExactSolanaPayloadMemoMismatch,
InvalidExactSolanaPayloadMemoCount,
Custom(CompactString),
}
impl ErrorReason {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::InsufficientFunds => "insufficient_funds",
Self::InvalidExactEvmPayloadAuthorizationValidAfter => {
"invalid_exact_evm_payload_authorization_valid_after"
}
Self::InvalidExactEvmPayloadAuthorizationValidBefore => {
"invalid_exact_evm_payload_authorization_valid_before"
}
Self::InvalidExactEvmPayloadAuthorizationValueMismatch => {
"invalid_exact_evm_payload_authorization_value_mismatch"
}
Self::InvalidExactEvmPayloadSignature => "invalid_exact_evm_payload_signature",
Self::InvalidExactEvmPayloadRecipientMismatch => {
"invalid_exact_evm_payload_recipient_mismatch"
}
Self::InvalidNetwork => "invalid_network",
Self::InvalidPayload => "invalid_payload",
Self::InvalidPaymentRequirements => "invalid_payment_requirements",
Self::InvalidScheme => "invalid_scheme",
Self::UnsupportedScheme => "unsupported_scheme",
Self::InvalidX402Version => "invalid_x402_version",
Self::InvalidTransactionState => "invalid_transaction_state",
Self::UnexpectedVerifyError => "unexpected_verify_error",
Self::UnexpectedSettleError => "unexpected_settle_error",
Self::DuplicateSettlement => "duplicate_settlement",
Self::NonceAlreadyUsed => "nonce_already_used",
Self::Permit2AllowanceRequired => "permit2_allowance_required",
Self::InvalidUptoEvmPayloadSettlementExceedsAmount => {
"invalid_upto_evm_payload_settlement_exceeds_amount"
}
Self::UptoFacilitatorMismatch => "upto_facilitator_mismatch",
Self::UptoUnauthorizedFacilitator => "upto_unauthorized_facilitator",
Self::UptoAmountExceedsPermitted => "upto_amount_exceeds_permitted",
Self::InvalidExactSolanaPayloadMemoMismatch => {
"invalid_exact_solana_payload_memo_mismatch"
}
Self::InvalidExactSolanaPayloadMemoCount => "invalid_exact_solana_payload_memo_count",
Self::Custom(s) => s.as_str(),
}
}
#[must_use]
pub fn from_wire(code: &str) -> Self {
match code {
"insufficient_funds" => Self::InsufficientFunds,
"invalid_exact_evm_payload_authorization_valid_after" => {
Self::InvalidExactEvmPayloadAuthorizationValidAfter
}
"invalid_exact_evm_payload_authorization_valid_before" => {
Self::InvalidExactEvmPayloadAuthorizationValidBefore
}
"invalid_exact_evm_payload_authorization_value_mismatch" => {
Self::InvalidExactEvmPayloadAuthorizationValueMismatch
}
"invalid_exact_evm_payload_signature" => Self::InvalidExactEvmPayloadSignature,
"invalid_exact_evm_payload_recipient_mismatch" => {
Self::InvalidExactEvmPayloadRecipientMismatch
}
"invalid_network" => Self::InvalidNetwork,
"invalid_payload" => Self::InvalidPayload,
"invalid_payment_requirements" => Self::InvalidPaymentRequirements,
"invalid_scheme" => Self::InvalidScheme,
"unsupported_scheme" => Self::UnsupportedScheme,
"invalid_x402_version" => Self::InvalidX402Version,
"invalid_transaction_state" => Self::InvalidTransactionState,
"unexpected_verify_error" => Self::UnexpectedVerifyError,
"unexpected_settle_error" => Self::UnexpectedSettleError,
"duplicate_settlement" => Self::DuplicateSettlement,
"nonce_already_used" => Self::NonceAlreadyUsed,
"permit2_allowance_required" => Self::Permit2AllowanceRequired,
"invalid_upto_evm_payload_settlement_exceeds_amount" => {
Self::InvalidUptoEvmPayloadSettlementExceedsAmount
}
"upto_facilitator_mismatch" => Self::UptoFacilitatorMismatch,
"upto_unauthorized_facilitator" => Self::UptoUnauthorizedFacilitator,
"upto_amount_exceeds_permitted" => Self::UptoAmountExceedsPermitted,
"invalid_exact_solana_payload_memo_mismatch" => {
Self::InvalidExactSolanaPayloadMemoMismatch
}
"invalid_exact_solana_payload_memo_count" => Self::InvalidExactSolanaPayloadMemoCount,
other => Self::Custom(CompactString::from(other)),
}
}
}
impl Display for ErrorReason {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for ErrorReason {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ErrorReason {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = CompactString::deserialize(deserializer)?;
Ok(Self::from_wire(&s))
}
}
impl From<&str> for ErrorReason {
fn from(value: &str) -> Self {
Self::from_wire(value)
}
}
impl From<CompactString> for ErrorReason {
fn from(value: CompactString) -> Self {
Self::from_wire(&value)
}
}
impl From<String> for ErrorReason {
fn from(value: String) -> Self {
Self::from_wire(&value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PaymentProblem {
reason: ErrorReason,
details: String,
}
impl PaymentProblem {
#[must_use]
pub const fn new(reason: ErrorReason, details: String) -> Self {
Self { reason, details }
}
#[must_use]
pub fn reason(&self) -> ErrorReason {
self.reason.clone()
}
#[must_use]
pub const fn reason_ref(&self) -> &ErrorReason {
&self.reason
}
#[must_use]
pub fn details(&self) -> &str {
&self.details
}
}
pub trait AsPaymentProblem {
fn as_payment_problem(&self) -> PaymentProblem;
}