1use std::fmt::{self, Display, Formatter};
21
22use compact_str::CompactString;
23use serde::{Deserialize, Deserializer, Serialize, Serializer};
24
25#[derive(Debug, thiserror::Error)]
30#[non_exhaustive]
31pub enum VerificationError {
32 #[error("invalid format: {0}")]
34 InvalidFormat(String),
35
36 #[error("payment amount is below requirements")]
41 InvalidPaymentAmount,
42
43 #[error("payment authorization is not yet valid")]
45 Early,
46
47 #[error("payment authorization has expired")]
49 Expired,
50
51 #[error("chain id mismatch")]
53 ChainIdMismatch,
54
55 #[error("payment recipient mismatch")]
57 RecipientMismatch,
58
59 #[error("payment asset mismatch")]
61 AssetMismatch,
62
63 #[error("insufficient on-chain balance")]
65 InsufficientFunds,
66
67 #[error("permit2 allowance required")]
73 Permit2AllowanceRequired,
74
75 #[error("invalid signature: {0}")]
77 InvalidSignature(String),
78
79 #[error("simulation failed: {0}")]
81 SimulationFailed(String),
82
83 #[error("unsupported chain")]
85 UnsupportedChain,
86
87 #[error("unsupported scheme")]
89 UnsupportedScheme,
90
91 #[error("accepted details do not match requirements")]
93 AcceptedRequirementsMismatch,
94
95 #[error("authorization nonce already used")]
97 NonceAlreadyUsed,
98
99 #[error("duplicate settlement attempt")]
101 DuplicateSettlement,
102
103 #[error("memo data mismatch")]
105 MemoMismatch,
106
107 #[error("memo instruction count invalid (expected 1, got {count})")]
109 MemoInstructionCountInvalid {
110 count: usize,
112 },
113
114 #[error("settlement amount {requested} exceeds authorised maximum {authorised}")]
121 SettlementAmountExceedsPermitted {
122 requested: String,
124 authorised: String,
126 },
127
128 #[error("witness.facilitator {witness} is not authorised on this facilitator")]
135 UptoFacilitatorMismatch {
136 witness: String,
138 },
139
140 #[error("on-chain proxy rejected settle: msg.sender does not match witness.facilitator")]
146 UptoUnauthorizedFacilitator,
147
148 #[error("on-chain proxy rejected settle: amount exceeds permitted maximum")]
154 UptoAmountExceedsPermitted,
155}
156
157impl From<serde_json::Error> for VerificationError {
158 fn from(err: serde_json::Error) -> Self {
159 Self::InvalidFormat(err.to_string())
160 }
161}
162
163impl AsPaymentProblem for VerificationError {
164 fn as_payment_problem(&self) -> PaymentProblem {
165 let reason = match self {
170 Self::InvalidFormat(_) | Self::InvalidSignature(_) | Self::Early | Self::Expired => {
171 ErrorReason::InvalidPayload
172 }
173 Self::InvalidPaymentAmount
174 | Self::RecipientMismatch
175 | Self::AssetMismatch
176 | Self::AcceptedRequirementsMismatch => ErrorReason::InvalidPaymentRequirements,
177 Self::ChainIdMismatch | Self::UnsupportedChain => ErrorReason::InvalidNetwork,
178 Self::InsufficientFunds => ErrorReason::InsufficientFunds,
179 Self::Permit2AllowanceRequired => ErrorReason::Permit2AllowanceRequired,
180 Self::SimulationFailed(_) => ErrorReason::InvalidTransactionState,
181 Self::UnsupportedScheme => ErrorReason::UnsupportedScheme,
182 Self::NonceAlreadyUsed => ErrorReason::NonceAlreadyUsed,
183 Self::DuplicateSettlement => ErrorReason::DuplicateSettlement,
184 Self::MemoMismatch => ErrorReason::InvalidExactSolanaPayloadMemoMismatch,
185 Self::MemoInstructionCountInvalid { .. } => {
186 ErrorReason::InvalidExactSolanaPayloadMemoCount
187 }
188 Self::SettlementAmountExceedsPermitted { .. } => {
189 ErrorReason::InvalidUptoEvmPayloadSettlementExceedsAmount
190 }
191 Self::UptoFacilitatorMismatch { .. } => ErrorReason::UptoFacilitatorMismatch,
192 Self::UptoUnauthorizedFacilitator => ErrorReason::UptoUnauthorizedFacilitator,
193 Self::UptoAmountExceedsPermitted => ErrorReason::UptoAmountExceedsPermitted,
194 };
195 PaymentProblem::new(reason, self.to_string())
196 }
197}
198
199#[derive(Debug, thiserror::Error)]
201#[non_exhaustive]
202pub enum SettlementError {
203 #[error("on-chain settlement failed: {0}")]
205 Onchain(String),
206
207 #[error("settlement timed out")]
209 Timeout,
210
211 #[error("duplicate settlement attempt")]
213 Duplicate,
214}
215
216impl AsPaymentProblem for SettlementError {
217 fn as_payment_problem(&self) -> PaymentProblem {
218 let reason = match self {
219 Self::Onchain(_) => ErrorReason::InvalidTransactionState,
220 Self::Timeout => ErrorReason::UnexpectedSettleError,
221 Self::Duplicate => ErrorReason::DuplicateSettlement,
222 };
223 PaymentProblem::new(reason, self.to_string())
224 }
225}
226
227#[derive(Debug, thiserror::Error)]
229#[non_exhaustive]
230pub enum FacilitatorError {
231 #[error(transparent)]
233 Verification(#[from] VerificationError),
234
235 #[error(transparent)]
237 Settlement(#[from] SettlementError),
238
239 #[error("{reason}: {message}")]
241 Aborted {
242 reason: String,
244 message: String,
246 },
247
248 #[error("on-chain error: {0}")]
250 Onchain(String),
251
252 #[error(transparent)]
254 Internal(Box<dyn std::error::Error + Send + Sync>),
255}
256
257impl FacilitatorError {
258 #[must_use]
260 pub fn aborted(reason: impl Into<String>, message: impl Into<String>) -> Self {
261 Self::Aborted {
262 reason: reason.into(),
263 message: message.into(),
264 }
265 }
266
267 #[must_use]
269 pub fn internal<E>(err: E) -> Self
270 where
271 E: Into<Box<dyn std::error::Error + Send + Sync>>,
272 {
273 Self::Internal(err.into())
274 }
275}
276
277impl AsPaymentProblem for FacilitatorError {
278 fn as_payment_problem(&self) -> PaymentProblem {
279 match self {
280 Self::Verification(e) => e.as_payment_problem(),
281 Self::Settlement(e) => e.as_payment_problem(),
282 Self::Aborted { reason, message } => PaymentProblem::new(
285 ErrorReason::from_wire(reason),
286 format!("{reason}: {message}"),
287 ),
288 Self::Onchain(message) => {
289 PaymentProblem::new(ErrorReason::InvalidTransactionState, message.clone())
290 }
291 Self::Internal(e) => {
292 PaymentProblem::new(ErrorReason::UnexpectedVerifyError, e.to_string())
293 }
294 }
295 }
296}
297
298#[derive(Debug, thiserror::Error)]
300#[non_exhaustive]
301pub enum ClientError {
302 #[error("no matching payment option")]
304 NoMatchingPaymentOption,
305
306 #[error("request is not cloneable (streaming body?)")]
308 RequestNotCloneable,
309
310 #[error("failed to parse 402 response: {0}")]
312 Parse(String),
313
314 #[error("failed to sign payment: {0}")]
316 Signing(String),
317
318 #[error("payment pre-condition not met: {0}")]
320 PreConditionFailed(String),
321
322 #[error(transparent)]
324 Json(#[from] serde_json::Error),
325}
326
327#[derive(Debug, Clone, PartialEq, Eq, Hash)]
336#[non_exhaustive]
337pub enum ErrorReason {
338 InsufficientFunds,
342 InvalidExactEvmPayloadAuthorizationValidAfter,
345 InvalidExactEvmPayloadAuthorizationValidBefore,
348 InvalidExactEvmPayloadAuthorizationValueMismatch,
351 InvalidExactEvmPayloadSignature,
354 InvalidExactEvmPayloadRecipientMismatch,
357 InvalidNetwork,
360 InvalidPayload,
363 InvalidPaymentRequirements,
366 InvalidScheme,
369 UnsupportedScheme,
372 InvalidX402Version,
375 InvalidTransactionState,
378 UnexpectedVerifyError,
381 UnexpectedSettleError,
384
385 DuplicateSettlement,
389 NonceAlreadyUsed,
392 Permit2AllowanceRequired,
397
398 InvalidUptoEvmPayloadSettlementExceedsAmount,
404 UptoFacilitatorMismatch,
408 UptoUnauthorizedFacilitator,
412 UptoAmountExceedsPermitted,
415
416 InvalidExactSolanaPayloadMemoMismatch,
420 InvalidExactSolanaPayloadMemoCount,
424
425 Custom(CompactString),
429}
430
431impl ErrorReason {
432 #[must_use]
434 pub fn as_str(&self) -> &str {
435 match self {
436 Self::InsufficientFunds => "insufficient_funds",
437 Self::InvalidExactEvmPayloadAuthorizationValidAfter => {
438 "invalid_exact_evm_payload_authorization_valid_after"
439 }
440 Self::InvalidExactEvmPayloadAuthorizationValidBefore => {
441 "invalid_exact_evm_payload_authorization_valid_before"
442 }
443 Self::InvalidExactEvmPayloadAuthorizationValueMismatch => {
444 "invalid_exact_evm_payload_authorization_value_mismatch"
445 }
446 Self::InvalidExactEvmPayloadSignature => "invalid_exact_evm_payload_signature",
447 Self::InvalidExactEvmPayloadRecipientMismatch => {
448 "invalid_exact_evm_payload_recipient_mismatch"
449 }
450 Self::InvalidNetwork => "invalid_network",
451 Self::InvalidPayload => "invalid_payload",
452 Self::InvalidPaymentRequirements => "invalid_payment_requirements",
453 Self::InvalidScheme => "invalid_scheme",
454 Self::UnsupportedScheme => "unsupported_scheme",
455 Self::InvalidX402Version => "invalid_x402_version",
456 Self::InvalidTransactionState => "invalid_transaction_state",
457 Self::UnexpectedVerifyError => "unexpected_verify_error",
458 Self::UnexpectedSettleError => "unexpected_settle_error",
459 Self::DuplicateSettlement => "duplicate_settlement",
460 Self::NonceAlreadyUsed => "nonce_already_used",
461 Self::Permit2AllowanceRequired => "permit2_allowance_required",
462 Self::InvalidUptoEvmPayloadSettlementExceedsAmount => {
463 "invalid_upto_evm_payload_settlement_exceeds_amount"
464 }
465 Self::UptoFacilitatorMismatch => "upto_facilitator_mismatch",
466 Self::UptoUnauthorizedFacilitator => "upto_unauthorized_facilitator",
467 Self::UptoAmountExceedsPermitted => "upto_amount_exceeds_permitted",
468 Self::InvalidExactSolanaPayloadMemoMismatch => {
469 "invalid_exact_solana_payload_memo_mismatch"
470 }
471 Self::InvalidExactSolanaPayloadMemoCount => "invalid_exact_solana_payload_memo_count",
472 Self::Custom(s) => s.as_str(),
473 }
474 }
475
476 #[must_use]
480 pub fn from_wire(code: &str) -> Self {
481 match code {
482 "insufficient_funds" => Self::InsufficientFunds,
483 "invalid_exact_evm_payload_authorization_valid_after" => {
484 Self::InvalidExactEvmPayloadAuthorizationValidAfter
485 }
486 "invalid_exact_evm_payload_authorization_valid_before" => {
487 Self::InvalidExactEvmPayloadAuthorizationValidBefore
488 }
489 "invalid_exact_evm_payload_authorization_value_mismatch" => {
490 Self::InvalidExactEvmPayloadAuthorizationValueMismatch
491 }
492 "invalid_exact_evm_payload_signature" => Self::InvalidExactEvmPayloadSignature,
493 "invalid_exact_evm_payload_recipient_mismatch" => {
494 Self::InvalidExactEvmPayloadRecipientMismatch
495 }
496 "invalid_network" => Self::InvalidNetwork,
497 "invalid_payload" => Self::InvalidPayload,
498 "invalid_payment_requirements" => Self::InvalidPaymentRequirements,
499 "invalid_scheme" => Self::InvalidScheme,
500 "unsupported_scheme" => Self::UnsupportedScheme,
501 "invalid_x402_version" => Self::InvalidX402Version,
502 "invalid_transaction_state" => Self::InvalidTransactionState,
503 "unexpected_verify_error" => Self::UnexpectedVerifyError,
504 "unexpected_settle_error" => Self::UnexpectedSettleError,
505 "duplicate_settlement" => Self::DuplicateSettlement,
506 "nonce_already_used" => Self::NonceAlreadyUsed,
507 "permit2_allowance_required" => Self::Permit2AllowanceRequired,
508 "invalid_upto_evm_payload_settlement_exceeds_amount" => {
509 Self::InvalidUptoEvmPayloadSettlementExceedsAmount
510 }
511 "upto_facilitator_mismatch" => Self::UptoFacilitatorMismatch,
512 "upto_unauthorized_facilitator" => Self::UptoUnauthorizedFacilitator,
513 "upto_amount_exceeds_permitted" => Self::UptoAmountExceedsPermitted,
514 "invalid_exact_solana_payload_memo_mismatch" => {
515 Self::InvalidExactSolanaPayloadMemoMismatch
516 }
517 "invalid_exact_solana_payload_memo_count" => Self::InvalidExactSolanaPayloadMemoCount,
518 other => Self::Custom(CompactString::from(other)),
519 }
520 }
521}
522
523impl Display for ErrorReason {
524 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
525 f.write_str(self.as_str())
526 }
527}
528
529impl Serialize for ErrorReason {
530 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
531 serializer.serialize_str(self.as_str())
532 }
533}
534
535impl<'de> Deserialize<'de> for ErrorReason {
536 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
537 let s = CompactString::deserialize(deserializer)?;
538 Ok(Self::from_wire(&s))
539 }
540}
541
542impl From<&str> for ErrorReason {
543 fn from(value: &str) -> Self {
544 Self::from_wire(value)
545 }
546}
547
548impl From<CompactString> for ErrorReason {
549 fn from(value: CompactString) -> Self {
550 Self::from_wire(&value)
551 }
552}
553
554impl From<String> for ErrorReason {
555 fn from(value: String) -> Self {
556 Self::from_wire(&value)
557 }
558}
559
560#[derive(Debug, Clone, PartialEq, Eq)]
566pub struct PaymentProblem {
567 reason: ErrorReason,
568 details: String,
569}
570
571impl PaymentProblem {
572 #[must_use]
574 pub const fn new(reason: ErrorReason, details: String) -> Self {
575 Self { reason, details }
576 }
577
578 #[must_use]
581 pub fn reason(&self) -> ErrorReason {
582 self.reason.clone()
583 }
584
585 #[must_use]
587 pub const fn reason_ref(&self) -> &ErrorReason {
588 &self.reason
589 }
590
591 #[must_use]
593 pub fn details(&self) -> &str {
594 &self.details
595 }
596}
597
598pub trait AsPaymentProblem {
600 fn as_payment_problem(&self) -> PaymentProblem;
602}