1use compact_str::CompactString;
4use serde::{Deserialize, Serialize};
5
6use super::{Extensions, Version};
7use crate::error::{ErrorReason, FacilitatorError, VerificationError};
8use crate::scheme::SchemeSlug;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct TypedVerifyRequest<const V: u8, TPayload, TRequirements> {
14 pub x402_version: Version<V>,
16 pub payment_payload: TPayload,
18 pub payment_requirements: TRequirements,
20}
21
22impl<const V: u8, TPayload, TRequirements> TypedVerifyRequest<V, TPayload, TRequirements>
23where
24 Self: serde::de::DeserializeOwned,
25{
26 pub fn from_verify(request: VerifyRequest) -> Result<Self, VerificationError> {
32 serde_json::from_value(request.into_json())
33 .map_err(|e| VerificationError::InvalidFormat(e.to_string()))
34 }
35}
36
37impl<const V: u8, TPayload, TRequirements> TryFrom<TypedVerifyRequest<V, TPayload, TRequirements>>
38 for VerifyRequest
39where
40 TPayload: Serialize,
41 TRequirements: Serialize,
42{
43 type Error = serde_json::Error;
44 fn try_from(
45 value: TypedVerifyRequest<V, TPayload, TRequirements>,
46 ) -> Result<Self, Self::Error> {
47 let json = serde_json::to_value(value)?;
48 Ok(Self(json))
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct VerifyRequest(serde_json::Value);
55
56impl VerifyRequest {
57 #[must_use]
59 pub fn into_json(self) -> serde_json::Value {
60 self.0
61 }
62
63 #[must_use]
65 pub fn scheme_slug(&self) -> Option<SchemeSlug> {
66 super::scheme_slug_from_json(&self.0)
67 }
68
69 #[must_use]
71 pub fn network(&self) -> &str {
72 super::network_from_json(&self.0)
73 }
74}
75
76impl From<serde_json::Value> for VerifyRequest {
77 fn from(value: serde_json::Value) -> Self {
78 Self(value)
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(into = "VerifyResponseWire", try_from = "VerifyResponseWire")]
88#[non_exhaustive]
89pub enum VerifyResponse {
90 Valid {
92 payer: Option<CompactString>,
94 extensions: Extensions,
96 extension_responses: Extensions,
98 extra: Option<serde_json::Value>,
100 },
101 Invalid {
103 reason: Option<ErrorReason>,
105 message: Option<CompactString>,
107 payer: Option<CompactString>,
109 extensions: Extensions,
111 extension_responses: Extensions,
113 extra: Option<serde_json::Value>,
115 },
116}
117
118impl VerifyResponse {
119 #[must_use]
121 pub fn valid(payer: impl Into<CompactString>) -> Self {
122 Self::Valid {
123 payer: Some(payer.into()),
124 extensions: Extensions::new(),
125 extension_responses: Extensions::new(),
126 extra: None,
127 }
128 }
129
130 #[must_use]
132 pub fn invalid(payer: Option<CompactString>, reason: ErrorReason) -> Self {
133 Self::Invalid {
134 reason: Some(reason),
135 message: None,
136 payer,
137 extensions: Extensions::new(),
138 extension_responses: Extensions::new(),
139 extra: None,
140 }
141 }
142
143 #[must_use]
145 pub fn invalid_with_message(
146 payer: Option<CompactString>,
147 reason: ErrorReason,
148 message: impl Into<CompactString>,
149 ) -> Self {
150 Self::Invalid {
151 reason: Some(reason),
152 message: Some(message.into()),
153 payer,
154 extensions: Extensions::new(),
155 extension_responses: Extensions::new(),
156 extra: None,
157 }
158 }
159
160 #[must_use]
162 pub const fn is_valid(&self) -> bool {
163 matches!(self, Self::Valid { .. })
164 }
165
166 #[must_use]
168 pub const fn extension_responses(&self) -> &Extensions {
169 match self {
170 Self::Valid {
171 extension_responses,
172 ..
173 }
174 | Self::Invalid {
175 extension_responses,
176 ..
177 } => extension_responses,
178 }
179 }
180
181 pub fn set_extension_responses(&mut self, responses: Extensions) {
183 match self {
184 Self::Valid {
185 extension_responses,
186 ..
187 }
188 | Self::Invalid {
189 extension_responses,
190 ..
191 } => *extension_responses = responses,
192 }
193 }
194
195 #[must_use]
200 pub fn from_facilitator_error(error: &FacilitatorError) -> Option<Self> {
201 let problem = error.as_payment_problem()?;
202 Some(Self::Invalid {
203 reason: Some(problem.reason()),
204 message: Some(CompactString::from(problem.details())),
205 payer: None,
206 extensions: Extensions::new(),
207 extension_responses: Extensions::new(),
208 extra: None,
209 })
210 }
211}
212
213#[derive(Serialize, Deserialize)]
214#[serde(rename_all = "camelCase", deny_unknown_fields)]
215struct VerifyResponseWire {
216 is_valid: bool,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
218 payer: Option<CompactString>,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 invalid_reason: Option<ErrorReason>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 invalid_message: Option<CompactString>,
223 #[serde(default, skip_serializing_if = "Extensions::is_empty")]
224 extensions: Extensions,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 extra: Option<serde_json::Value>,
227}
228
229impl From<VerifyResponse> for VerifyResponseWire {
230 fn from(value: VerifyResponse) -> Self {
231 match value {
232 VerifyResponse::Valid {
233 payer,
234 extensions,
235 extra,
236 extension_responses: _,
237 } => Self {
238 is_valid: true,
239 payer,
240 invalid_reason: None,
241 invalid_message: None,
242 extensions,
243 extra,
244 },
245 VerifyResponse::Invalid {
246 reason,
247 message,
248 payer,
249 extensions,
250 extra,
251 extension_responses: _,
252 } => Self {
253 is_valid: false,
254 payer,
255 invalid_reason: reason,
256 invalid_message: message,
257 extensions,
258 extra,
259 },
260 }
261 }
262}
263
264impl TryFrom<VerifyResponseWire> for VerifyResponse {
265 type Error = String;
266 fn try_from(wire: VerifyResponseWire) -> Result<Self, Self::Error> {
267 if wire.is_valid {
268 Ok(Self::Valid {
269 payer: wire.payer,
270 extensions: wire.extensions,
271 extension_responses: Extensions::new(),
272 extra: wire.extra,
273 })
274 } else {
275 Ok(Self::Invalid {
276 reason: wire.invalid_reason,
277 message: wire.invalid_message,
278 payer: wire.payer,
279 extensions: wire.extensions,
280 extension_responses: Extensions::new(),
281 extra: wire.extra,
282 })
283 }
284 }
285}
286
287#[cfg(test)]
288#[allow(
289 clippy::unwrap_used,
290 clippy::panic,
291 clippy::indexing_slicing,
292 reason = "unit tests panic on assertion failure"
293)]
294mod tests {
295 use serde_json::json;
296
297 use super::*;
298 use crate::error::FacilitatorTransportKind;
299 use crate::payment::ExtensionEntry;
300
301 fn v2_json(network: &str, scheme: &str) -> serde_json::Value {
302 json!({
303 "x402Version": 2,
304 "paymentPayload": {
305 "accepted": { "network": network, "scheme": scheme }
306 },
307 "paymentRequirements": { "network": network }
308 })
309 }
310
311 #[test]
312 fn verify_request_scheme_slug_evm() {
313 let req = VerifyRequest::from(v2_json("eip155:8453", "exact"));
314 let slug = req.scheme_slug().unwrap();
315 assert_eq!(slug.to_string(), "eip155:8453:exact");
316 }
317
318 #[test]
319 fn slug_rejects_wrong_version() {
320 let mut json = v2_json("eip155:1", "exact");
321 json["x402Version"] = json!(99);
322 assert!(super::super::scheme_slug_from_json(&json).is_none());
323 }
324
325 #[test]
326 fn slug_rejects_invalid_caip2() {
327 assert!(super::super::scheme_slug_from_json(&v2_json("not-a-caip2", "exact")).is_none());
328 }
329
330 #[test]
331 fn verify_valid_roundtrip() {
332 let response = VerifyResponse::valid("0xABC");
333 let encoded = serde_json::to_value(&response).unwrap();
334 assert_eq!(encoded["isValid"], true);
335 assert_eq!(encoded["payer"], "0xABC");
336 assert!(encoded.get("invalidReason").is_none());
337 assert!(encoded.get("extensionResponses").is_none());
338
339 let back: VerifyResponse = serde_json::from_value(encoded).unwrap();
340 assert_eq!(back, response);
341 }
342
343 #[test]
344 fn verify_valid_with_extensions_roundtrip() {
345 let mut extensions = Extensions::new();
346 extensions.insert(
347 "payment-identifier",
348 ExtensionEntry::info(json!({"id": "order-123"})),
349 );
350 let response = VerifyResponse::Valid {
351 payer: Some("0xABC".into()),
352 extensions,
353 extension_responses: Extensions::new(),
354 extra: None,
355 };
356 let encoded = serde_json::to_value(&response).unwrap();
357 assert_eq!(
358 encoded["extensions"]["payment-identifier"]["info"]["id"],
359 "order-123"
360 );
361 let back: VerifyResponse = serde_json::from_value(encoded).unwrap();
362 assert_eq!(back, response);
363 }
364
365 #[test]
366 fn verify_invalid_roundtrip() {
367 let response = VerifyResponse::invalid_with_message(
368 Some("0xDEF".into()),
369 ErrorReason::InsufficientFunds,
370 "not enough USDC",
371 );
372 let encoded = serde_json::to_value(&response).unwrap();
373 assert_eq!(encoded["isValid"], false);
374 assert_eq!(encoded["invalidReason"], "insufficient_funds");
375 assert_eq!(encoded["invalidMessage"], "not enough USDC");
376 let back: VerifyResponse = serde_json::from_value(encoded).unwrap();
377 assert_eq!(back, response);
378 }
379
380 #[test]
381 fn extension_responses_are_not_serialized() {
382 let mut response = VerifyResponse::valid("0xABC");
383 let mut side = Extensions::new();
384 side.insert("bazaar", ExtensionEntry::raw(json!({"status": "success"})));
385 response.set_extension_responses(side);
386 let encoded = serde_json::to_value(&response).unwrap();
387 assert!(encoded.get("extensionResponses").is_none());
388 assert!(encoded.get("extensions").is_none());
389 assert!(!response.extension_responses().is_empty());
390 }
391
392 #[test]
393 fn from_facilitator_error_refuses_transport() {
394 let err = FacilitatorError::transport(FacilitatorTransportKind::MalformedSuccessBody);
395 assert!(VerifyResponse::from_facilitator_error(&err).is_none());
396 }
397
398 #[test]
399 fn verify_deny_unknown_top_level_fields() {
400 let verify_typo = json!({
401 "isValid": true,
402 "payer": "0xABC",
403 "extraTypo": {"k": 1}
404 });
405 assert!(serde_json::from_value::<VerifyResponse>(verify_typo).is_err());
406 }
407
408 #[test]
409 fn verify_valid_omitted_payer() {
410 let json = json!({ "isValid": true });
411 let back: VerifyResponse = serde_json::from_value(json).unwrap();
412 assert!(matches!(back, VerifyResponse::Valid { payer: None, .. }));
413 let encoded = serde_json::to_value(&back).unwrap();
414 assert!(encoded.get("payer").is_none());
415 }
416
417 #[test]
418 fn verify_invalid_omitted_reason_and_payer() {
419 let json = json!({ "isValid": false });
420 let back: VerifyResponse = serde_json::from_value(json).unwrap();
421 assert!(matches!(
422 back,
423 VerifyResponse::Invalid {
424 reason: None,
425 payer: None,
426 ..
427 }
428 ));
429 let encoded = serde_json::to_value(&back).unwrap();
430 assert!(encoded.get("invalidReason").is_none());
431 assert!(encoded.get("payer").is_none());
432 }
433}