Skip to main content

r402_core/
error.rs

1//! Layered error types for x402 facilitator operations.
2//!
3//! The hierarchy is:
4//!
5//! ```text
6//! FacilitatorError
7//! ├── Verification(VerificationError)
8//! ├── Settlement(SettlementError)
9//! ├── Aborted { reason, message }   // hook-level veto
10//! ├── Onchain(String)               // chain RPC / tx revert
11//! └── Internal(Box<dyn Error>)      // unexpected internal failure
12//! ```
13//!
14//! Every leaf carries enough information to round-trip through the wire via
15//! [`AsPaymentProblem`], which chain-specific crates implement for their own
16//! error variants.
17//!
18//! [`ErrorReason`] is the machine-readable wire code (x402 v2 spec §9).
19
20use std::fmt::{self, Display, Formatter};
21
22use compact_str::CompactString;
23use serde::{Deserialize, Deserializer, Serialize, Serializer};
24
25/// Verification-phase failures.
26///
27/// Returned when payload parsing, signature verification, or pre-settlement
28/// on-chain checks reject the request.
29#[derive(Debug, thiserror::Error)]
30#[non_exhaustive]
31pub enum VerificationError {
32    /// The payload JSON / binary format could not be parsed.
33    #[error("invalid format: {0}")]
34    InvalidFormat(String),
35
36    /// The payment amount is below the required amount.
37    ///
38    /// For SVM exact, overpayment (`actual >= required`) is allowed per
39    /// `scheme_exact_svm.md` §1.4; only underpayment maps here.
40    #[error("payment amount is below requirements")]
41    InvalidPaymentAmount,
42
43    /// The payment authorization is not yet valid.
44    #[error("payment authorization is not yet valid")]
45    Early,
46
47    /// The payment authorization has expired.
48    #[error("payment authorization has expired")]
49    Expired,
50
51    /// The chain ID does not match requirements.
52    #[error("chain id mismatch")]
53    ChainIdMismatch,
54
55    /// The payment recipient does not match requirements.
56    #[error("payment recipient mismatch")]
57    RecipientMismatch,
58
59    /// The token asset does not match requirements.
60    #[error("payment asset mismatch")]
61    AssetMismatch,
62
63    /// On-chain balance is insufficient.
64    #[error("insufficient on-chain balance")]
65    InsufficientFunds,
66
67    /// Permit2 allowance is insufficient.
68    ///
69    /// Clients receiving this should call `approve` on the token contract
70    /// pointing at the Permit2 address and retry. HTTP transports map this
71    /// to `412 Precondition Failed`.
72    #[error("permit2 allowance required")]
73    Permit2AllowanceRequired,
74
75    /// The payment signature is invalid.
76    #[error("invalid signature: {0}")]
77    InvalidSignature(String),
78
79    /// Pre-settlement simulation failed.
80    #[error("simulation failed: {0}")]
81    SimulationFailed(String),
82
83    /// The chain is not supported by this facilitator.
84    #[error("unsupported chain")]
85    UnsupportedChain,
86
87    /// The scheme is not supported by this facilitator.
88    #[error("unsupported scheme")]
89    UnsupportedScheme,
90
91    /// Accepted details diverge from declared requirements.
92    #[error("accepted details do not match requirements")]
93    AcceptedRequirementsMismatch,
94
95    /// EIP-3009 nonce already consumed on-chain.
96    #[error("authorization nonce already used")]
97    NonceAlreadyUsed,
98
99    /// Attempted to settle a transaction whose payload was already processed.
100    #[error("duplicate settlement attempt")]
101    DuplicateSettlement,
102
103    /// Memo data does not match `extra.memo`.
104    #[error("memo data mismatch")]
105    MemoMismatch,
106
107    /// Invalid number of memo instructions (spec requires exactly one).
108    #[error("memo instruction count invalid (expected 1, got {count})")]
109    MemoInstructionCountInvalid {
110        /// Observed number of memo instructions.
111        count: usize,
112    },
113
114    /// Resource server requested a settlement amount exceeding the signed
115    /// maximum authorisation (upto scheme).
116    ///
117    /// Maps to `ErrorReason::InvalidUptoEvmPayloadSettlementExceedsAmount`
118    /// on the wire, which serialises as
119    /// `invalid_upto_evm_payload_settlement_exceeds_amount` per x402 v2 spec.
120    #[error("settlement amount {requested} exceeds authorised maximum {authorised}")]
121    SettlementAmountExceedsPermitted {
122        /// Amount the resource server asked to settle.
123        requested: String,
124        /// Maximum amount signed by the buyer.
125        authorised: String,
126    },
127
128    /// `witness.facilitator` does not match any address known to the
129    /// facilitator (upto scheme).
130    ///
131    /// Buyer either signed with a stale `paymentRequirements.extra.facilitatorAddress`
132    /// or attempted to bind the payment to a different facilitator instance.
133    /// Maps to `ErrorReason::UptoFacilitatorMismatch`.
134    #[error("witness.facilitator {witness} is not authorised on this facilitator")]
135    UptoFacilitatorMismatch {
136        /// EIP-55 checksummed `witness.facilitator` from the buyer's signed payload.
137        witness: String,
138    },
139
140    /// On-chain proxy reverted with `UnauthorizedFacilitator` because the
141    /// settle transaction was submitted from an address other than
142    /// `witness.facilitator` (upto scheme).
143    ///
144    /// Maps to `ErrorReason::UptoUnauthorizedFacilitator`.
145    #[error("on-chain proxy rejected settle: msg.sender does not match witness.facilitator")]
146    UptoUnauthorizedFacilitator,
147
148    /// On-chain proxy reverted with `AmountExceedsPermitted` (upto scheme).
149    /// Defence-in-depth signal: the off-chain pipeline should have caught
150    /// this before submission.
151    ///
152    /// Maps to `ErrorReason::UptoAmountExceedsPermitted`.
153    #[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        // Chain-agnostic mappings target the spec §9 standard codes.
166        // Chain-specific facilitators may override with chain-prefixed
167        // codes (e.g. `invalid_exact_evm_payload_signature`) at their own
168        // `AsPaymentProblem` impls.
169        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/// Settlement-phase failures.
200#[derive(Debug, thiserror::Error)]
201#[non_exhaustive]
202pub enum SettlementError {
203    /// Settlement reverted on-chain or the RPC call failed.
204    #[error("on-chain settlement failed: {0}")]
205    Onchain(String),
206
207    /// Settlement timed out.
208    #[error("settlement timed out")]
209    Timeout,
210
211    /// Duplicate settlement detected at the cache layer.
212    #[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/// Top-level facilitator failure.
228#[derive(Debug, thiserror::Error)]
229#[non_exhaustive]
230pub enum FacilitatorError {
231    /// Verification-phase failure.
232    #[error(transparent)]
233    Verification(#[from] VerificationError),
234
235    /// Settlement-phase failure.
236    #[error(transparent)]
237    Settlement(#[from] SettlementError),
238
239    /// Hook veto: the operation was aborted before or during its lifecycle.
240    #[error("{reason}: {message}")]
241    Aborted {
242        /// Machine-readable abort reason (e.g. `"kyt_blocked"`).
243        reason: String,
244        /// Human-readable abort description.
245        message: String,
246    },
247
248    /// On-chain or RPC failure that is not scheme-specific.
249    #[error("on-chain error: {0}")]
250    Onchain(String),
251
252    /// Any other internal error not covered above.
253    #[error(transparent)]
254    Internal(Box<dyn std::error::Error + Send + Sync>),
255}
256
257impl FacilitatorError {
258    /// Constructs an aborted variant from reason and message strings.
259    #[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    /// Constructs an internal error from any boxed error.
268    #[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            // Hook-supplied reason strings round-trip through `from_wire`,
283            // which preserves any unknown code as `Custom(…)`.
284            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/// Client-side error returned when constructing / signing payments.
299#[derive(Debug, thiserror::Error)]
300#[non_exhaustive]
301pub enum ClientError {
302    /// No candidate matched the client's capabilities.
303    #[error("no matching payment option")]
304    NoMatchingPaymentOption,
305
306    /// The underlying HTTP body cannot be retried (streaming, etc.).
307    #[error("request is not cloneable (streaming body?)")]
308    RequestNotCloneable,
309
310    /// Parsing the 402 response body failed.
311    #[error("failed to parse 402 response: {0}")]
312    Parse(String),
313
314    /// Signing the authorization failed.
315    #[error("failed to sign payment: {0}")]
316    Signing(String),
317
318    /// A required on-chain pre-condition is missing (e.g. approval).
319    #[error("payment pre-condition not met: {0}")]
320    PreConditionFailed(String),
321
322    /// JSON (de)serialisation failed.
323    #[error(transparent)]
324    Json(#[from] serde_json::Error),
325}
326
327/// Canonical error code returned on the wire when a payment fails.
328///
329/// The variants form a closed set of well-known codes the SDK can pattern
330/// match against. Any unknown wire code is preserved via [`Self::Custom`]
331/// so deserialisation is total and idempotent.
332///
333/// The enum is `#[non_exhaustive]`: new well-known variants may be added
334/// in minor releases without breaking semver.
335#[derive(Debug, Clone, PartialEq, Eq, Hash)]
336#[non_exhaustive]
337pub enum ErrorReason {
338    // Spec §9 standard codes.
339    /// Client does not have enough tokens to complete the payment.
340    /// Wire code: `insufficient_funds`.
341    InsufficientFunds,
342    /// Payment authorization is not yet valid (before validAfter timestamp).
343    /// Wire code: `invalid_exact_evm_payload_authorization_valid_after`.
344    InvalidExactEvmPayloadAuthorizationValidAfter,
345    /// Payment authorization has expired (after validBefore timestamp).
346    /// Wire code: `invalid_exact_evm_payload_authorization_valid_before`.
347    InvalidExactEvmPayloadAuthorizationValidBefore,
348    /// Payment amount does not exactly match the required amount.
349    /// Wire code: `invalid_exact_evm_payload_authorization_value_mismatch`.
350    InvalidExactEvmPayloadAuthorizationValueMismatch,
351    /// Payment authorization signature is invalid or improperly signed.
352    /// Wire code: `invalid_exact_evm_payload_signature`.
353    InvalidExactEvmPayloadSignature,
354    /// Recipient address does not match payment requirements.
355    /// Wire code: `invalid_exact_evm_payload_recipient_mismatch`.
356    InvalidExactEvmPayloadRecipientMismatch,
357    /// Specified blockchain network is not supported.
358    /// Wire code: `invalid_network`.
359    InvalidNetwork,
360    /// Payment payload is malformed or contains invalid data.
361    /// Wire code: `invalid_payload`.
362    InvalidPayload,
363    /// Payment requirements object is invalid or malformed.
364    /// Wire code: `invalid_payment_requirements`.
365    InvalidPaymentRequirements,
366    /// Specified payment scheme is not supported.
367    /// Wire code: `invalid_scheme`.
368    InvalidScheme,
369    /// Payment scheme is not supported by the facilitator.
370    /// Wire code: `unsupported_scheme`.
371    UnsupportedScheme,
372    /// Protocol version is not supported.
373    /// Wire code: `invalid_x402_version`.
374    InvalidX402Version,
375    /// Blockchain transaction failed or was rejected.
376    /// Wire code: `invalid_transaction_state`.
377    InvalidTransactionState,
378    /// Unexpected error occurred during payment verification.
379    /// Wire code: `unexpected_verify_error`.
380    UnexpectedVerifyError,
381    /// Unexpected error occurred during payment settlement.
382    /// Wire code: `unexpected_settle_error`.
383    UnexpectedSettleError,
384
385    // Cross-chain reserved codes (SDK consensus).
386    /// Facilitator detected a duplicate settlement attempt and rejected it.
387    /// Wire code: `duplicate_settlement`.
388    DuplicateSettlement,
389    /// EIP-3009 authorization nonce has already been consumed on-chain.
390    /// Wire code: `nonce_already_used`.
391    NonceAlreadyUsed,
392    /// Permit2 allowance is insufficient; the payer must call `approve`
393    /// on the token contract pointing at the Permit2 address first.
394    /// Wire code: `permit2_allowance_required`.
395    /// HTTP mapping: `412 Precondition Failed`.
396    Permit2AllowanceRequired,
397
398    // Upto scheme codes.
399    /// Resource server requested a settlement amount exceeding the buyer's
400    /// signed maximum authorisation. Reserved by
401    /// `schemes/upto/scheme_upto_evm.md` §4.
402    /// Wire code: `invalid_upto_evm_payload_settlement_exceeds_amount`.
403    InvalidUptoEvmPayloadSettlementExceedsAmount,
404    /// `witness.facilitator` doesn't match any of the facilitator's signer
405    /// addresses. Buyer signed a stale or unauthorised facilitator address.
406    /// Wire code: `upto_facilitator_mismatch`.
407    UptoFacilitatorMismatch,
408    /// On-chain proxy reverted with `UnauthorizedFacilitator` (msg.sender
409    /// is not the witness-bound facilitator).
410    /// Wire code: `upto_unauthorized_facilitator`.
411    UptoUnauthorizedFacilitator,
412    /// On-chain proxy reverted with `AmountExceedsPermitted`.
413    /// Wire code: `upto_amount_exceeds_permitted`.
414    UptoAmountExceedsPermitted,
415
416    // SVM exact scheme codes (chain-prefixed).
417    /// SVM `extra.memo` field did not match the memo instruction data.
418    /// Wire code: `invalid_exact_solana_payload_memo_mismatch`.
419    InvalidExactSolanaPayloadMemoMismatch,
420    /// Number of memo instructions attached is invalid (spec requires
421    /// exactly one when `extra.memo` is declared).
422    /// Wire code: `invalid_exact_solana_payload_memo_count`.
423    InvalidExactSolanaPayloadMemoCount,
424
425    // Catch-all preserving the wire code.
426    /// Catch-all preserving any unknown wire code verbatim. Round-trips
427    /// losslessly through serialisation so clients never lose information.
428    Custom(CompactString),
429}
430
431impl ErrorReason {
432    /// Returns the wire-format code (`snake_case` per spec §9).
433    #[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    /// Constructs an [`ErrorReason`] from any wire code, mapping known
477    /// strings to their canonical variants and preserving unknown codes
478    /// as [`Self::Custom`].
479    #[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/// A structured payment-problem record returned through wire boundaries.
561///
562/// Not itself serialisable — it is an internal, strongly-typed bridge between
563/// Rust errors and [`crate::wire::VerifyResponse::Invalid`] /
564/// [`crate::wire::SettleResponse::Failure`] on the wire.
565#[derive(Debug, Clone, PartialEq, Eq)]
566pub struct PaymentProblem {
567    reason: ErrorReason,
568    details: String,
569}
570
571impl PaymentProblem {
572    /// Creates a new problem record.
573    #[must_use]
574    pub const fn new(reason: ErrorReason, details: String) -> Self {
575        Self { reason, details }
576    }
577
578    /// Returns the wire-level reason code (clones the inner string for the
579    /// `Custom` variant; cheap for unit variants).
580    #[must_use]
581    pub fn reason(&self) -> ErrorReason {
582        self.reason.clone()
583    }
584
585    /// Returns a borrowed reference to the wire-level reason code.
586    #[must_use]
587    pub const fn reason_ref(&self) -> &ErrorReason {
588        &self.reason
589    }
590
591    /// Returns the human-readable details.
592    #[must_use]
593    pub fn details(&self) -> &str {
594        &self.details
595    }
596}
597
598/// Trait for converting errors into [`PaymentProblem`]s.
599pub trait AsPaymentProblem {
600    /// Produces the canonical wire-level payment problem.
601    fn as_payment_problem(&self) -> PaymentProblem;
602}