1use compact_str::CompactString;
4use serde::{Deserialize, Serialize};
5
6use super::verify::{TypedVerifyRequest, VerifyRequest};
7use super::{Base64Bytes, Extensions};
8use crate::error::{ErrorReason, FacilitatorError, VerificationError};
9use crate::scheme::SchemeSlug;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct SettleRequest(serde_json::Value);
14
15impl SettleRequest {
16 #[must_use]
18 pub fn into_json(self) -> serde_json::Value {
19 self.0
20 }
21
22 #[must_use]
24 pub fn scheme_slug(&self) -> Option<SchemeSlug> {
25 super::scheme_slug_from_json(&self.0)
26 }
27
28 #[must_use]
30 pub fn network(&self) -> &str {
31 super::network_from_json(&self.0)
32 }
33
34 pub fn set_settlement_amount(&mut self, amount: &str) -> Result<(), VerificationError> {
41 let req = self
42 .0
43 .get_mut("paymentRequirements")
44 .and_then(serde_json::Value::as_object_mut)
45 .ok_or_else(|| {
46 VerificationError::InvalidFormat(
47 "settle request missing paymentRequirements object".into(),
48 )
49 })?;
50 let _ = req.insert(
51 "amount".to_owned(),
52 serde_json::Value::String(amount.to_owned()),
53 );
54 Ok(())
55 }
56}
57
58impl From<serde_json::Value> for SettleRequest {
59 fn from(value: serde_json::Value) -> Self {
60 Self(value)
61 }
62}
63
64impl From<VerifyRequest> for SettleRequest {
65 fn from(request: VerifyRequest) -> Self {
66 Self(request.into_json())
67 }
68}
69
70#[allow(
71 clippy::multiple_inherent_impl,
72 reason = "from_settle is owned by the settle request type"
73)]
74impl<const V: u8, TPayload, TRequirements> TypedVerifyRequest<V, TPayload, TRequirements>
75where
76 Self: serde::de::DeserializeOwned,
77{
78 pub fn from_settle(request: SettleRequest) -> Result<Self, VerificationError> {
84 serde_json::from_value(request.into_json())
85 .map_err(|e| VerificationError::InvalidFormat(e.to_string()))
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(into = "SettleResponseWire", try_from = "SettleResponseWire")]
95#[non_exhaustive]
96pub enum SettleResponse {
97 Success {
99 payer: Option<CompactString>,
101 transaction: CompactString,
103 network: CompactString,
105 amount: Option<CompactString>,
107 extensions: Extensions,
109 extension_responses: Extensions,
111 extra: Option<serde_json::Value>,
113 },
114 Failure {
116 reason: ErrorReason,
118 message: Option<CompactString>,
120 payer: Option<CompactString>,
122 transaction: CompactString,
125 network: CompactString,
127 extensions: Extensions,
129 extension_responses: Extensions,
131 extra: Option<serde_json::Value>,
133 },
134}
135
136impl SettleResponse {
137 #[must_use]
139 pub const fn is_success(&self) -> bool {
140 matches!(self, Self::Success { .. })
141 }
142
143 #[must_use]
145 pub const fn extension_responses(&self) -> &Extensions {
146 match self {
147 Self::Success {
148 extension_responses,
149 ..
150 }
151 | Self::Failure {
152 extension_responses,
153 ..
154 } => extension_responses,
155 }
156 }
157
158 pub const fn extensions_mut(&mut self) -> &mut Extensions {
160 match self {
161 Self::Success { extensions, .. } | Self::Failure { extensions, .. } => extensions,
162 }
163 }
164
165 pub fn set_extension_responses(&mut self, responses: Extensions) {
167 match self {
168 Self::Success {
169 extension_responses,
170 ..
171 }
172 | Self::Failure {
173 extension_responses,
174 ..
175 } => *extension_responses = responses,
176 }
177 }
178
179 #[must_use]
184 pub fn encode_base64(&self) -> Option<Base64Bytes> {
185 if !self.is_success() {
186 return None;
187 }
188 let json = serde_json::to_vec(self).ok()?;
189 Some(Base64Bytes::encode(json))
190 }
191
192 #[must_use]
194 pub fn encode_base64_any(&self) -> Option<Base64Bytes> {
195 let json = serde_json::to_vec(self).ok()?;
196 Some(Base64Bytes::encode(json))
197 }
198
199 #[must_use]
205 pub fn from_facilitator_error(
206 error: &FacilitatorError,
207 network: impl Into<CompactString>,
208 transaction: impl Into<CompactString>,
209 ) -> Option<Self> {
210 let problem = error.as_payment_problem()?;
211 let reason = problem.reason();
212 let transaction = transaction.into();
213 debug_assert!(
214 reason != ErrorReason::SettlementPending || !transaction.is_empty(),
215 "settlement_pending requires a non-empty transaction hash"
216 );
217 Some(Self::Failure {
218 reason,
219 message: Some(CompactString::from(problem.details())),
220 payer: None,
221 transaction,
222 network: network.into(),
223 extensions: Extensions::new(),
224 extension_responses: Extensions::new(),
225 extra: None,
226 })
227 }
228
229 #[must_use]
231 pub fn is_retryable_settlement_pending(&self) -> bool {
232 match self {
233 Self::Failure {
234 reason: ErrorReason::SettlementPending,
235 transaction,
236 ..
237 } => !transaction.is_empty(),
238 _ => false,
239 }
240 }
241}
242
243#[derive(Serialize, Deserialize)]
244#[serde(rename_all = "camelCase", deny_unknown_fields)]
245struct SettleResponseWire {
246 success: bool,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
248 error_reason: Option<ErrorReason>,
249 #[serde(default, skip_serializing_if = "Option::is_none")]
250 error_message: Option<CompactString>,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
252 payer: Option<CompactString>,
253 transaction: CompactString,
254 network: CompactString,
255 #[serde(default, skip_serializing_if = "Option::is_none")]
256 amount: Option<CompactString>,
257 #[serde(default, skip_serializing_if = "Extensions::is_empty")]
258 extensions: Extensions,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
260 extra: Option<serde_json::Value>,
261}
262
263impl From<SettleResponse> for SettleResponseWire {
264 fn from(value: SettleResponse) -> Self {
265 match value {
266 SettleResponse::Success {
267 payer,
268 transaction,
269 network,
270 amount,
271 extensions,
272 extra,
273 extension_responses: _,
274 } => Self {
275 success: true,
276 error_reason: None,
277 error_message: None,
278 payer,
279 transaction,
280 network,
281 amount,
282 extensions,
283 extra,
284 },
285 SettleResponse::Failure {
286 reason,
287 message,
288 payer,
289 transaction,
290 network,
291 extensions,
292 extra,
293 extension_responses: _,
294 } => Self {
295 success: false,
296 error_reason: Some(reason),
297 error_message: message,
298 payer,
299 transaction,
300 network,
301 amount: None,
302 extensions,
303 extra,
304 },
305 }
306 }
307}
308
309impl TryFrom<SettleResponseWire> for SettleResponse {
310 type Error = String;
311 fn try_from(wire: SettleResponseWire) -> Result<Self, Self::Error> {
312 if wire.success {
313 Ok(Self::Success {
314 payer: wire.payer,
315 transaction: wire.transaction,
316 network: wire.network,
317 amount: wire.amount,
318 extensions: wire.extensions,
319 extension_responses: Extensions::new(),
320 extra: wire.extra,
321 })
322 } else {
323 Ok(Self::Failure {
324 reason: wire.error_reason.ok_or("missing field: errorReason")?,
325 message: wire.error_message,
326 payer: wire.payer,
327 transaction: wire.transaction,
328 network: wire.network,
329 extensions: wire.extensions,
330 extension_responses: Extensions::new(),
331 extra: wire.extra,
332 })
333 }
334 }
335}
336
337#[cfg(test)]
338#[allow(
339 clippy::unwrap_used,
340 clippy::panic,
341 clippy::indexing_slicing,
342 reason = "unit tests panic on assertion failure"
343)]
344mod tests {
345 use serde_json::json;
346
347 use super::*;
348 use crate::error::FacilitatorTransportKind;
349 use crate::payment::ExtensionEntry;
350
351 fn v2_json(network: &str, scheme: &str) -> serde_json::Value {
352 json!({
353 "x402Version": 2,
354 "paymentPayload": {
355 "accepted": { "network": network, "scheme": scheme }
356 },
357 "paymentRequirements": { "network": network }
358 })
359 }
360
361 #[test]
362 fn settle_request_from_verify_preserves_slug() {
363 let verify = VerifyRequest::from(v2_json("eip155:42161", "exact"));
364 let settle: SettleRequest = verify.into();
365 assert_eq!(
366 settle.scheme_slug().unwrap().to_string(),
367 "eip155:42161:exact"
368 );
369 }
370
371 #[test]
372 fn settle_request_network_missing_returns_empty() {
373 let settle = SettleRequest::from(serde_json::json!({}));
374 assert_eq!(settle.network(), "");
375 }
376
377 #[test]
378 fn settle_amount_override_rewrites_payment_requirements() {
379 let mut settle = SettleRequest::from(json!({
380 "x402Version": 2,
381 "paymentPayload": { "accepted": { "network": "eip155:8453", "scheme": "upto" } },
382 "paymentRequirements": { "network": "eip155:8453", "amount": "5000000" }
383 }));
384 settle.set_settlement_amount("1500000").unwrap();
385 let json = settle.into_json();
386 assert_eq!(
387 json["paymentRequirements"]["amount"].as_str(),
388 Some("1500000")
389 );
390 }
391
392 #[test]
393 fn settle_amount_override_errors_when_requirements_missing() {
394 let mut settle = SettleRequest::from(json!({}));
395 let err = settle.set_settlement_amount("1").unwrap_err();
396 assert!(matches!(err, VerificationError::InvalidFormat(_)));
397 }
398
399 #[test]
400 fn settle_success_with_amount_roundtrip() {
401 let response = SettleResponse::Success {
402 payer: Some("0xABC".into()),
403 transaction: "0xTX".into(),
404 network: "eip155:8453".into(),
405 amount: Some("1000000".into()),
406 extensions: Extensions::new(),
407 extension_responses: Extensions::new(),
408 extra: None,
409 };
410 let encoded = serde_json::to_value(&response).unwrap();
411 assert_eq!(encoded["success"], true);
412 assert_eq!(encoded["amount"], "1000000");
413 assert!(encoded.get("extensionResponses").is_none());
414 let back: SettleResponse = serde_json::from_value(encoded).unwrap();
415 assert_eq!(back, response);
416 }
417
418 #[test]
419 fn settle_success_without_amount_is_valid() {
420 let response = SettleResponse::Success {
421 payer: Some("0xABC".into()),
422 transaction: "0xTX".into(),
423 network: "eip155:1".into(),
424 amount: None,
425 extensions: Extensions::new(),
426 extension_responses: Extensions::new(),
427 extra: None,
428 };
429 let encoded = serde_json::to_value(&response).unwrap();
430 assert!(encoded.get("amount").is_none());
431 let back: SettleResponse = serde_json::from_value(encoded).unwrap();
432 assert_eq!(back, response);
433 }
434
435 #[test]
436 fn settle_success_empty_transaction_allowed() {
437 let json = json!({
438 "success": true,
439 "payer": "0xABC",
440 "transaction": "",
441 "network": "eip155:1"
442 });
443 let back: SettleResponse = serde_json::from_value(json).unwrap();
444 assert!(matches!(
445 back,
446 SettleResponse::Success {
447 ref transaction,
448 payer: Some(ref payer),
449 ..
450 } if transaction.is_empty() && payer == "0xABC"
451 ));
452 }
453
454 #[test]
455 fn settle_success_empty_transaction_omitted_payer() {
456 let json = json!({
457 "success": true,
458 "transaction": "",
459 "network": "eip155:1"
460 });
461 let back: SettleResponse = serde_json::from_value(json).unwrap();
462 assert!(matches!(
463 back,
464 SettleResponse::Success {
465 ref transaction,
466 payer: None,
467 ..
468 } if transaction.is_empty()
469 ));
470 let encoded = serde_json::to_value(&back).unwrap();
471 assert_eq!(encoded["transaction"], "");
472 assert!(encoded.get("payer").is_none());
473 }
474
475 #[test]
476 fn settle_failure_roundtrip() {
477 let response = SettleResponse::Failure {
478 reason: ErrorReason::DuplicateSettlement,
479 message: Some("already processed".into()),
480 payer: None,
481 transaction: CompactString::default(),
482 network: "solana:mainnet".into(),
483 extensions: Extensions::new(),
484 extension_responses: Extensions::new(),
485 extra: None,
486 };
487 let encoded = serde_json::to_value(&response).unwrap();
488 assert_eq!(encoded["errorReason"], "duplicate_settlement");
489 assert_eq!(encoded["transaction"], "");
490 let back: SettleResponse = serde_json::from_value(encoded).unwrap();
491 assert_eq!(back, response);
492 }
493
494 #[test]
495 fn settle_failure_serializes_empty_transaction() {
496 let response = SettleResponse::Failure {
497 reason: ErrorReason::UnexpectedSettleError,
498 message: None,
499 payer: None,
500 transaction: CompactString::default(),
501 network: "eip155:8453".into(),
502 extensions: Extensions::new(),
503 extension_responses: Extensions::new(),
504 extra: None,
505 };
506 let encoded = serde_json::to_value(&response).unwrap();
507 assert_eq!(encoded["transaction"], "");
508 }
509
510 #[test]
511 fn settle_failure_pending_roundtrip_preserves_transaction() {
512 let response = SettleResponse::Failure {
513 reason: ErrorReason::SettlementPending,
514 message: Some("rpc timeout waiting for receipt".into()),
515 payer: Some("0xpayer".into()),
516 transaction: "0xabc".into(),
517 network: "eip155:8453".into(),
518 extensions: Extensions::new(),
519 extension_responses: Extensions::new(),
520 extra: None,
521 };
522 let encoded = serde_json::to_value(&response).unwrap();
523 assert_eq!(encoded["success"], false);
524 assert_eq!(encoded["errorReason"], "settlement_pending");
525 assert_eq!(encoded["transaction"], "0xabc");
526 assert!(encoded.get("extra").is_none());
527 let back: SettleResponse = serde_json::from_value(encoded).unwrap();
528 assert_eq!(back, response);
529 assert!(back.is_retryable_settlement_pending());
530 }
531
532 #[test]
533 fn settle_failure_pending_empty_transaction_is_not_retryable() {
534 let response = SettleResponse::Failure {
535 reason: ErrorReason::SettlementPending,
536 message: None,
537 payer: None,
538 transaction: CompactString::default(),
539 network: "eip155:8453".into(),
540 extensions: Extensions::new(),
541 extension_responses: Extensions::new(),
542 extra: None,
543 };
544 assert!(!response.is_retryable_settlement_pending());
545 }
546
547 #[test]
548 fn verify_and_settle_extra_roundtrip() {
549 let extra = json!({"assetTransferMethod": "eip3009"});
550 let settle = SettleResponse::Success {
551 payer: Some("0xABC".into()),
552 transaction: "0xTX".into(),
553 network: "eip155:1".into(),
554 amount: None,
555 extensions: Extensions::new(),
556 extension_responses: Extensions::new(),
557 extra: Some(extra.clone()),
558 };
559 let settle_json = serde_json::to_value(&settle).unwrap();
560 assert_eq!(settle_json["extra"]["assetTransferMethod"], "eip3009");
561 let settle_back: SettleResponse = serde_json::from_value(settle_json).unwrap();
562 assert_eq!(settle_back, settle);
563
564 let failure = SettleResponse::Failure {
565 reason: ErrorReason::UnexpectedSettleError,
566 message: None,
567 payer: None,
568 transaction: CompactString::default(),
569 network: "eip155:1".into(),
570 extensions: Extensions::new(),
571 extension_responses: Extensions::new(),
572 extra: Some(extra),
573 };
574 let failure_json = serde_json::to_value(&failure).unwrap();
575 assert_eq!(failure_json["extra"]["assetTransferMethod"], "eip3009");
576 let failure_back: SettleResponse = serde_json::from_value(failure_json).unwrap();
577 assert_eq!(failure_back, failure);
578 }
579
580 #[test]
581 fn settle_deny_unknown_top_level_fields() {
582 let settle_typo = json!({
583 "success": true,
584 "payer": "0xABC",
585 "transaction": "0xTX",
586 "network": "eip155:1",
587 "extraTypo": {"k": 1}
588 });
589 assert!(serde_json::from_value::<SettleResponse>(settle_typo).is_err());
590 }
591
592 #[test]
593 fn from_facilitator_error_carries_transaction() {
594 let err = FacilitatorError::Onchain("rpc timeout".into());
595 let response =
596 SettleResponse::from_facilitator_error(&err, "eip155:8453", "0xpending").unwrap();
597 match response {
598 SettleResponse::Failure {
599 transaction,
600 network,
601 extra,
602 ..
603 } => {
604 assert_eq!(transaction, "0xpending");
605 assert_eq!(network, "eip155:8453");
606 assert!(extra.is_none());
607 }
608 SettleResponse::Success { .. } => panic!("expected failure"),
609 }
610 }
611
612 #[test]
613 fn from_facilitator_error_refuses_transport() {
614 let err = FacilitatorError::transport(FacilitatorTransportKind::Timeout);
615 assert!(SettleResponse::from_facilitator_error(&err, "eip155:8453", "").is_none());
616 }
617
618 #[test]
619 fn settle_encode_base64_only_for_success() {
620 let success = SettleResponse::Success {
621 payer: Some("0xA".into()),
622 transaction: "0xT".into(),
623 network: "eip155:1".into(),
624 amount: None,
625 extensions: Extensions::new(),
626 extension_responses: Extensions::new(),
627 extra: None,
628 };
629 assert!(success.encode_base64().is_some());
630
631 let failure = SettleResponse::Failure {
632 reason: ErrorReason::UnexpectedSettleError,
633 message: None,
634 payer: None,
635 transaction: CompactString::default(),
636 network: "eip155:1".into(),
637 extensions: Extensions::new(),
638 extension_responses: Extensions::new(),
639 extra: None,
640 };
641 assert!(failure.encode_base64().is_none());
642 assert!(failure.encode_base64_any().is_some());
643 }
644
645 #[test]
646 fn encode_base64_strips_extension_responses() {
647 let mut success = SettleResponse::Success {
648 payer: Some("0xA".into()),
649 transaction: "0xT".into(),
650 network: "eip155:1".into(),
651 amount: None,
652 extensions: Extensions::new(),
653 extension_responses: Extensions::new(),
654 extra: None,
655 };
656 let mut side = Extensions::new();
657 side.insert("bazaar", ExtensionEntry::raw(json!({"status": "success"})));
658 success.set_extension_responses(side);
659 let bytes = success.encode_base64().unwrap();
660 let json: serde_json::Value = serde_json::from_slice(&bytes.decode().unwrap()).unwrap();
661 assert!(json.get("extensionResponses").is_none());
662 }
663}