1use std::fmt::{self, Display, Formatter};
4
5use compact_str::CompactString;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
12#[non_exhaustive]
13pub enum ErrorReason {
14 InsufficientFunds,
16 InvalidExactEvmPayloadAuthorizationValidAfter,
18 InvalidExactEvmPayloadAuthorizationValidBefore,
20 InvalidExactEvmPayloadAuthorizationValueMismatch,
22 InvalidExactEvmPayloadSignature,
24 InvalidExactEvmPayloadRecipientMismatch,
26 InvalidNetwork,
28 InvalidPayload,
30 InvalidPaymentRequirements,
32 InvalidScheme,
34 UnsupportedScheme,
36 InvalidX402Version,
38 InvalidTransactionState,
40 UnexpectedVerifyError,
42 UnexpectedSettleError,
44 SettlementPending,
49 DuplicateSettlement,
51 NonceAlreadyUsed,
53 Permit2AllowanceRequired,
55 InvalidUptoEvmPayloadSettlementExceedsAmount,
57 UptoFacilitatorMismatch,
59 UptoUnauthorizedFacilitator,
61 UptoAmountExceedsPermitted,
63 InvalidExactSolanaPayloadMemoMismatch,
65 InvalidExactSolanaPayloadMemoCount,
67 IncompatibleSettlementMode,
69 SettlementAborted,
71 ExtensionEchoMismatch,
73 Custom(CompactString),
75}
76
77impl ErrorReason {
78 #[must_use]
80 pub fn as_str(&self) -> &str {
81 match self {
82 Self::InsufficientFunds => "insufficient_funds",
83 Self::InvalidExactEvmPayloadAuthorizationValidAfter => {
84 "invalid_exact_evm_payload_authorization_valid_after"
85 }
86 Self::InvalidExactEvmPayloadAuthorizationValidBefore => {
87 "invalid_exact_evm_payload_authorization_valid_before"
88 }
89 Self::InvalidExactEvmPayloadAuthorizationValueMismatch => {
90 "invalid_exact_evm_payload_authorization_value_mismatch"
91 }
92 Self::InvalidExactEvmPayloadSignature => "invalid_exact_evm_payload_signature",
93 Self::InvalidExactEvmPayloadRecipientMismatch => {
94 "invalid_exact_evm_payload_recipient_mismatch"
95 }
96 Self::InvalidNetwork => "invalid_network",
97 Self::InvalidPayload => "invalid_payload",
98 Self::InvalidPaymentRequirements => "invalid_payment_requirements",
99 Self::InvalidScheme => "invalid_scheme",
100 Self::UnsupportedScheme => "unsupported_scheme",
101 Self::InvalidX402Version => "invalid_x402_version",
102 Self::InvalidTransactionState => "invalid_transaction_state",
103 Self::UnexpectedVerifyError => "unexpected_verify_error",
104 Self::UnexpectedSettleError => "unexpected_settle_error",
105 Self::SettlementPending => "settlement_pending",
106 Self::DuplicateSettlement => "duplicate_settlement",
107 Self::NonceAlreadyUsed => "nonce_already_used",
108 Self::Permit2AllowanceRequired => "permit2_allowance_required",
109 Self::InvalidUptoEvmPayloadSettlementExceedsAmount => {
110 "invalid_upto_evm_payload_settlement_exceeds_amount"
111 }
112 Self::UptoFacilitatorMismatch => "upto_facilitator_mismatch",
113 Self::UptoUnauthorizedFacilitator => "upto_unauthorized_facilitator",
114 Self::UptoAmountExceedsPermitted => "upto_amount_exceeds_permitted",
115 Self::InvalidExactSolanaPayloadMemoMismatch => {
116 "invalid_exact_solana_payload_memo_mismatch"
117 }
118 Self::InvalidExactSolanaPayloadMemoCount => "invalid_exact_solana_payload_memo_count",
119 Self::IncompatibleSettlementMode => "incompatible_settlement_mode",
120 Self::SettlementAborted => "settlement_aborted",
121 Self::ExtensionEchoMismatch => "extension_echo_mismatch",
122 Self::Custom(s) => s.as_str(),
123 }
124 }
125
126 #[must_use]
128 pub fn from_wire(code: &str) -> Self {
129 match code {
130 "insufficient_funds" => Self::InsufficientFunds,
131 "invalid_exact_evm_payload_authorization_valid_after" => {
132 Self::InvalidExactEvmPayloadAuthorizationValidAfter
133 }
134 "invalid_exact_evm_payload_authorization_valid_before" => {
135 Self::InvalidExactEvmPayloadAuthorizationValidBefore
136 }
137 "invalid_exact_evm_payload_authorization_value_mismatch" => {
138 Self::InvalidExactEvmPayloadAuthorizationValueMismatch
139 }
140 "invalid_exact_evm_payload_signature" => Self::InvalidExactEvmPayloadSignature,
141 "invalid_exact_evm_payload_recipient_mismatch" => {
142 Self::InvalidExactEvmPayloadRecipientMismatch
143 }
144 "invalid_network" => Self::InvalidNetwork,
145 "invalid_payload" => Self::InvalidPayload,
146 "invalid_payment_requirements" => Self::InvalidPaymentRequirements,
147 "invalid_scheme" => Self::InvalidScheme,
148 "unsupported_scheme" => Self::UnsupportedScheme,
149 "invalid_x402_version" => Self::InvalidX402Version,
150 "invalid_transaction_state" => Self::InvalidTransactionState,
151 "unexpected_verify_error" => Self::UnexpectedVerifyError,
152 "unexpected_settle_error" => Self::UnexpectedSettleError,
153 "settlement_pending" => Self::SettlementPending,
154 "duplicate_settlement" => Self::DuplicateSettlement,
155 "nonce_already_used" => Self::NonceAlreadyUsed,
156 "permit2_allowance_required" => Self::Permit2AllowanceRequired,
157 "invalid_upto_evm_payload_settlement_exceeds_amount" => {
158 Self::InvalidUptoEvmPayloadSettlementExceedsAmount
159 }
160 "upto_facilitator_mismatch" => Self::UptoFacilitatorMismatch,
161 "upto_unauthorized_facilitator" => Self::UptoUnauthorizedFacilitator,
162 "upto_amount_exceeds_permitted" => Self::UptoAmountExceedsPermitted,
163 "invalid_exact_solana_payload_memo_mismatch" => {
164 Self::InvalidExactSolanaPayloadMemoMismatch
165 }
166 "invalid_exact_solana_payload_memo_count" => Self::InvalidExactSolanaPayloadMemoCount,
167 "incompatible_settlement_mode" => Self::IncompatibleSettlementMode,
168 "settlement_aborted" => Self::SettlementAborted,
169 "extension_echo_mismatch" => Self::ExtensionEchoMismatch,
170 other => Self::Custom(CompactString::from(other)),
171 }
172 }
173}
174
175impl Display for ErrorReason {
176 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
177 f.write_str(self.as_str())
178 }
179}
180
181impl Serialize for ErrorReason {
182 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
183 serializer.serialize_str(self.as_str())
184 }
185}
186
187impl<'de> Deserialize<'de> for ErrorReason {
188 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
189 let s = CompactString::deserialize(deserializer)?;
190 Ok(Self::from_wire(&s))
191 }
192}
193
194impl From<&str> for ErrorReason {
195 fn from(value: &str) -> Self {
196 Self::from_wire(value)
197 }
198}
199
200impl From<CompactString> for ErrorReason {
201 fn from(value: CompactString) -> Self {
202 Self::from_wire(&value)
203 }
204}
205
206impl From<String> for ErrorReason {
207 fn from(value: String) -> Self {
208 Self::from_wire(&value)
209 }
210}
211
212#[cfg(test)]
213#[allow(clippy::unwrap_used, reason = "unit tests panic on assertion failure")]
214mod tests {
215 use super::ErrorReason;
216
217 #[test]
218 fn settlement_pending_wire_code() {
219 assert_eq!(
220 ErrorReason::SettlementPending.as_str(),
221 "settlement_pending"
222 );
223 assert_eq!(
224 ErrorReason::from_wire("settlement_pending"),
225 ErrorReason::SettlementPending
226 );
227 let json = serde_json::to_value(ErrorReason::SettlementPending).unwrap();
228 assert_eq!(json, serde_json::json!("settlement_pending"));
229 let back: ErrorReason = serde_json::from_value(json).unwrap();
230 assert_eq!(back, ErrorReason::SettlementPending);
231 }
232
233 #[test]
234 fn incompatible_mode_and_aborted_roundtrip() {
235 assert_eq!(
236 ErrorReason::from_wire("incompatible_settlement_mode"),
237 ErrorReason::IncompatibleSettlementMode
238 );
239 assert_eq!(
240 ErrorReason::from_wire("settlement_aborted"),
241 ErrorReason::SettlementAborted
242 );
243 assert_eq!(
244 ErrorReason::IncompatibleSettlementMode.as_str(),
245 "incompatible_settlement_mode"
246 );
247 assert_eq!(
248 ErrorReason::SettlementAborted.as_str(),
249 "settlement_aborted"
250 );
251 assert_eq!(
252 ErrorReason::from_wire("extension_echo_mismatch"),
253 ErrorReason::ExtensionEchoMismatch
254 );
255 assert_eq!(
256 ErrorReason::ExtensionEchoMismatch.as_str(),
257 "extension_echo_mismatch"
258 );
259 }
260
261 #[test]
262 fn custom_preserves_unknown_code() {
263 let reason = ErrorReason::from_wire("vendor_specific_diagnostic_42");
264 assert_eq!(reason.as_str(), "vendor_specific_diagnostic_42");
265 assert_eq!(
266 reason,
267 ErrorReason::Custom(compact_str::CompactString::from(
268 "vendor_specific_diagnostic_42"
269 ))
270 );
271 }
272}