1mod hooks;
12
13use std::sync::Arc;
14use std::sync::atomic::{AtomicBool, Ordering};
15
16pub use hooks::{
17 AfterVerifyDecision, BeforeOpDecision, CancelReason, DynResourceServerHooks,
18 PaymentHookContext, ResourceServerHooks, SettleResultContext, SkipHandlerDirective,
19 VerifiedPaymentCanceledContext, VerifyResultContext, WirePaymentPayload,
20};
21use serde::Serialize;
22
23use crate::error::{FacilitatorError, VerificationError};
24use crate::facilitator::FailureRecovery;
25use crate::facilitator::{DynFacilitator, Facilitator};
26use crate::wire::{
27 PaymentRequirements, SettleRequest, SettleResponse, TypedVerifyRequest, V2, VerifyRequest,
28 VerifyResponse, find_matching_requirements,
29};
30use crate::wire::{
31 SettlementOverrides, asset_decimals_from_extra, resolve_settlement_override_amount,
32};
33
34#[derive(Debug, Clone)]
36#[non_exhaustive]
37pub struct VerifyPaymentOutcome {
38 pub response: VerifyResponse,
40 pub skip_handler: Option<SkipHandlerDirective>,
42}
43
44pub struct ResourceServer {
46 facilitator: Arc<dyn DynFacilitator>,
47 hooks: Vec<Arc<dyn DynResourceServerHooks>>,
48}
49
50impl Clone for ResourceServer {
51 fn clone(&self) -> Self {
52 Self {
53 facilitator: Arc::clone(&self.facilitator),
54 hooks: self.hooks.clone(),
55 }
56 }
57}
58
59impl std::fmt::Debug for ResourceServer {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 f.debug_struct("ResourceServer")
62 .field("hooks", &self.hooks.len())
63 .finish_non_exhaustive()
64 }
65}
66
67impl ResourceServer {
68 #[must_use]
70 pub fn new<F>(facilitator: Arc<F>) -> Self
71 where
72 F: Facilitator + 'static,
73 {
74 let erased: Arc<dyn DynFacilitator> = facilitator;
75 Self {
76 facilitator: erased,
77 hooks: Vec::new(),
78 }
79 }
80
81 #[must_use]
83 pub fn from_dyn(facilitator: Arc<dyn DynFacilitator>) -> Self {
84 Self {
85 facilitator,
86 hooks: Vec::new(),
87 }
88 }
89
90 #[must_use]
92 pub fn with_hook(mut self, hook: impl ResourceServerHooks + 'static) -> Self {
93 self.hooks.push(Arc::new(hook));
94 self
95 }
96
97 pub fn add_hook(&mut self, hook: impl ResourceServerHooks + 'static) {
99 self.hooks.push(Arc::new(hook));
100 }
101
102 #[must_use]
104 pub fn hook_count(&self) -> usize {
105 self.hooks.len()
106 }
107
108 #[must_use]
110 pub fn facilitator(&self) -> Arc<dyn DynFacilitator> {
111 Arc::clone(&self.facilitator)
112 }
113
114 #[must_use]
119 #[allow(
120 clippy::unused_self,
121 reason = "API parity with Go X402ResourceServer.FindMatchingRequirements"
122 )]
123 pub fn find_matching_requirements<'a>(
124 &self,
125 available: &'a [PaymentRequirements],
126 payload: &WirePaymentPayload,
127 ) -> Option<&'a PaymentRequirements> {
128 find_matching_requirements(available, &payload.accepted)
129 }
130
131 pub async fn verify_payment(
138 &self,
139 payload: &WirePaymentPayload,
140 requirements: &PaymentRequirements,
141 ) -> Result<VerifyPaymentOutcome, FacilitatorError> {
142 let payment = PaymentHookContext {
143 payload: payload.clone(),
144 requirements: requirements.clone(),
145 };
146
147 let mut skipped: Option<VerifyResponse> = None;
149 for hook in &self.hooks {
150 match hook.before_verify(&payment).await {
151 BeforeOpDecision::Continue => {}
152 BeforeOpDecision::Abort { reason, message } => {
153 return Err(FacilitatorError::Aborted { reason, message });
154 }
155 BeforeOpDecision::Skip { result } => {
156 skipped = Some(result);
157 break;
158 }
159 }
160 }
161
162 let response = if let Some(local) = skipped {
163 local
164 } else {
165 self.call_verify_with_failure_hooks(&payment, payload, requirements)
166 .await?
167 };
168
169 if !response.is_valid() {
173 return Ok(VerifyPaymentOutcome {
174 response,
175 skip_handler: None,
176 });
177 }
178
179 let result_ctx = VerifyResultContext {
180 payment: payment.clone(),
181 result: response.clone(),
182 };
183
184 let mut skip_handler: Option<SkipHandlerDirective> = None;
185 for hook in &self.hooks {
186 match hook.after_verify(&result_ctx).await {
187 AfterVerifyDecision::Continue => {}
188 AfterVerifyDecision::Abort { reason, message } => {
189 self.notify_verified_payment_canceled(
190 payload,
191 requirements,
192 CancelReason::AfterVerifyAborted,
193 Some(message.as_str()),
194 None,
195 )
196 .await;
197 return Err(FacilitatorError::Aborted { reason, message });
198 }
199 AfterVerifyDecision::SkipHandler {
200 response: directive,
201 } => {
202 skip_handler = Some(directive);
204 }
205 }
206 }
207
208 Ok(VerifyPaymentOutcome {
209 response,
210 skip_handler,
211 })
212 }
213
214 pub async fn settle_payment(
226 &self,
227 payload: &WirePaymentPayload,
228 requirements: &PaymentRequirements,
229 overrides: Option<&SettlementOverrides>,
230 ) -> Result<SettleResponse, FacilitatorError> {
231 let effective = apply_settlement_overrides(requirements, overrides)?;
232 let payment = PaymentHookContext {
233 payload: payload.clone(),
234 requirements: effective.clone(),
235 };
236
237 let mut skipped: Option<SettleResponse> = None;
238 for hook in &self.hooks {
239 match hook.before_settle(&payment).await {
240 BeforeOpDecision::Continue => {}
241 BeforeOpDecision::Abort { reason, message } => {
242 return Err(FacilitatorError::Aborted { reason, message });
243 }
244 BeforeOpDecision::Skip { result } => {
245 skipped = Some(result);
246 break;
247 }
248 }
249 }
250
251 let response = if let Some(local) = skipped {
252 local
253 } else {
254 self.call_settle_with_failure_hooks(&payment, payload, &effective)
255 .await?
256 };
257
258 let result_ctx = SettleResultContext {
261 payment,
262 result: response.clone(),
263 };
264 for hook in &self.hooks {
265 hook.after_settle(&result_ctx).await;
266 }
267
268 Ok(response)
269 }
270
271 pub async fn notify_verified_payment_canceled(
277 &self,
278 payload: &WirePaymentPayload,
279 requirements: &PaymentRequirements,
280 reason: CancelReason,
281 error: Option<&str>,
282 response_status: Option<u16>,
283 ) {
284 if self.hooks.is_empty() {
285 return;
286 }
287 let ctx = VerifiedPaymentCanceledContext {
288 payment: PaymentHookContext {
289 payload: payload.clone(),
290 requirements: requirements.clone(),
291 },
292 reason,
293 error: error.map(str::to_owned),
294 response_status,
295 };
296 for hook in &self.hooks {
297 hook.on_verified_payment_canceled(&ctx).await;
298 }
299 }
300
301 #[must_use]
303 pub fn cancellation_guard(
304 &self,
305 payload: WirePaymentPayload,
306 requirements: PaymentRequirements,
307 ) -> CancellationGuard {
308 CancellationGuard {
309 server: self.clone(),
310 payload,
311 requirements,
312 fired: AtomicBool::new(false),
313 }
314 }
315
316 async fn call_verify_with_failure_hooks(
317 &self,
318 payment: &PaymentHookContext,
319 payload: &WirePaymentPayload,
320 requirements: &PaymentRequirements,
321 ) -> Result<VerifyResponse, FacilitatorError> {
322 let request = build_verify_request(payload, requirements)?;
323 match DynFacilitator::verify(self.facilitator.as_ref(), request).await {
324 Ok(r) if r.is_valid() => Ok(r),
325 Ok(invalid) => {
326 let error = facilitator_error_from_invalid(&invalid);
327 self.recover_verify(payment, error).await.or(Ok(invalid))
328 }
329 Err(error) => self.recover_verify(payment, error).await,
330 }
331 }
332
333 async fn recover_verify(
334 &self,
335 payment: &PaymentHookContext,
336 error: FacilitatorError,
337 ) -> Result<VerifyResponse, FacilitatorError> {
338 for hook in &self.hooks {
339 if let FailureRecovery::Recovered(r) = hook.on_verify_failure(payment, &error).await {
340 return Ok(r);
341 }
342 }
343 Err(error)
344 }
345
346 async fn call_settle_with_failure_hooks(
347 &self,
348 payment: &PaymentHookContext,
349 payload: &WirePaymentPayload,
350 requirements: &PaymentRequirements,
351 ) -> Result<SettleResponse, FacilitatorError> {
352 let request = build_settle_request(payload, requirements)?;
353 match DynFacilitator::settle(self.facilitator.as_ref(), request).await {
354 Ok(r) => Ok(r),
355 Err(error) => self.recover_settle(payment, error).await,
356 }
357 }
358
359 async fn recover_settle(
360 &self,
361 payment: &PaymentHookContext,
362 error: FacilitatorError,
363 ) -> Result<SettleResponse, FacilitatorError> {
364 for hook in &self.hooks {
365 if let FailureRecovery::Recovered(r) = hook.on_settle_failure(payment, &error).await {
366 return Ok(r);
367 }
368 }
369 Err(error)
370 }
371}
372
373#[derive(Debug)]
375pub struct CancellationGuard {
376 server: ResourceServer,
377 payload: WirePaymentPayload,
378 requirements: PaymentRequirements,
379 fired: AtomicBool,
380}
381
382impl CancellationGuard {
383 pub async fn cancel(
385 &self,
386 reason: CancelReason,
387 error: Option<&str>,
388 response_status: Option<u16>,
389 ) {
390 if self
391 .fired
392 .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
393 .is_err()
394 {
395 return;
396 }
397 self.server
398 .notify_verified_payment_canceled(
399 &self.payload,
400 &self.requirements,
401 reason,
402 error,
403 response_status,
404 )
405 .await;
406 }
407
408 #[must_use]
410 pub fn has_fired(&self) -> bool {
411 self.fired.load(Ordering::SeqCst)
412 }
413}
414
415fn build_verify_request(
416 payload: &WirePaymentPayload,
417 requirements: &PaymentRequirements,
418) -> Result<VerifyRequest, FacilitatorError> {
419 let typed = TypedVerifyRequest {
420 x402_version: V2,
421 payment_payload: payload.clone(),
422 payment_requirements: requirements.clone(),
423 };
424 to_verify_request(&typed)
425}
426
427fn build_settle_request(
428 payload: &WirePaymentPayload,
429 requirements: &PaymentRequirements,
430) -> Result<SettleRequest, FacilitatorError> {
431 let typed = TypedVerifyRequest {
432 x402_version: V2,
433 payment_payload: payload.clone(),
434 payment_requirements: requirements.clone(),
435 };
436 let verify = to_verify_request(&typed)?;
437 Ok(SettleRequest::from(verify.into_json()))
438}
439
440fn to_verify_request<T: Serialize>(typed: &T) -> Result<VerifyRequest, FacilitatorError> {
441 let json = serde_json::to_value(typed).map_err(FacilitatorError::internal)?;
442 Ok(VerifyRequest::from(json))
443}
444
445fn apply_settlement_overrides(
447 requirements: &PaymentRequirements,
448 overrides: Option<&SettlementOverrides>,
449) -> Result<PaymentRequirements, FacilitatorError> {
450 let Some(overrides) = overrides else {
451 return Ok(requirements.clone());
452 };
453 let Some(raw) = overrides
454 .amount
455 .as_ref()
456 .map(|s| s.trim())
457 .filter(|s| !s.is_empty())
458 else {
459 return Ok(requirements.clone());
460 };
461 let decimals = asset_decimals_from_extra(requirements.extra.as_ref());
462 let resolved = resolve_settlement_override_amount(raw, requirements.amount.as_str(), decimals)
463 .map_err(|e| {
464 FacilitatorError::Verification(VerificationError::InvalidFormat(format!(
465 "invalid settlement override: {e}"
466 )))
467 })?;
468 let mut effective = requirements.clone();
469 effective.amount = resolved.into();
470 Ok(effective)
471}
472
473fn facilitator_error_from_invalid(invalid: &VerifyResponse) -> FacilitatorError {
474 match invalid {
475 VerifyResponse::Invalid {
476 reason, message, ..
477 } => {
478 let detail = message
479 .as_ref()
480 .map_or_else(|| reason.to_string(), |m| format!("{reason}: {m}"));
481 FacilitatorError::Verification(VerificationError::InvalidFormat(detail))
482 }
483 VerifyResponse::Valid { .. } => FacilitatorError::Verification(
484 VerificationError::InvalidFormat("expected invalid verify response".into()),
485 ),
486 }
487}
488
489#[cfg(test)]
490#[allow(
491 clippy::excessive_nesting,
492 reason = "hook + mock facilitator tests nest async bodies"
493)]
494mod tests {
495 use std::future::Future;
496 use std::sync::atomic::{AtomicUsize, Ordering};
497
498 use super::*;
499 use crate::wire::{Extensions, SupportedResponse};
500
501 struct MockFacilitator {
502 verifies: AtomicUsize,
503 settles: AtomicUsize,
504 fail_verify: bool,
505 fail_settle: bool,
506 }
507
508 impl Facilitator for MockFacilitator {
509 fn verify(
510 &self,
511 _request: VerifyRequest,
512 ) -> impl Future<Output = Result<VerifyResponse, FacilitatorError>> + Send {
513 self.verifies.fetch_add(1, Ordering::SeqCst);
514 let result = if self.fail_verify {
515 Err(FacilitatorError::Onchain("mock verify".into()))
516 } else {
517 Ok(VerifyResponse::valid("0xpayer"))
518 };
519 std::future::ready(result)
520 }
521
522 fn settle(
523 &self,
524 _request: SettleRequest,
525 ) -> impl Future<Output = Result<SettleResponse, FacilitatorError>> + Send {
526 self.settles.fetch_add(1, Ordering::SeqCst);
527 let result = if self.fail_settle {
528 Err(FacilitatorError::Onchain("mock settle".into()))
529 } else {
530 Ok(SettleResponse::Success {
531 payer: "0xpayer".into(),
532 transaction: "0xtx".into(),
533 network: "eip155:1".into(),
534 amount: Some("1".into()),
535 extensions: Extensions::new(),
536 })
537 };
538 std::future::ready(result)
539 }
540
541 fn supported(
542 &self,
543 ) -> impl Future<Output = Result<SupportedResponse, FacilitatorError>> + Send {
544 std::future::ready(Ok(SupportedResponse::default()))
545 }
546 }
547
548 fn sample_payload() -> WirePaymentPayload {
549 let req = PaymentRequirements::new(
550 "exact".into(),
551 "eip155:1".parse().unwrap(),
552 "1".into(),
553 "0xa".into(),
554 "0xb".into(),
555 60,
556 );
557 WirePaymentPayload::new(req, serde_json::json!({"k": 1}))
558 }
559
560 fn mock_ok() -> Arc<MockFacilitator> {
561 Arc::new(MockFacilitator {
562 verifies: AtomicUsize::new(0),
563 settles: AtomicUsize::new(0),
564 fail_verify: false,
565 fail_settle: false,
566 })
567 }
568
569 #[tokio::test]
570 async fn verify_and_settle_invoke_facilitator() {
571 let mock = mock_ok();
572 let rs = ResourceServer::new(Arc::clone(&mock));
573 let payload = sample_payload();
574 let req = payload.accepted.clone();
575 let out = rs.verify_payment(&payload, &req).await.unwrap();
576 assert!(out.response.is_valid());
577 assert!(out.skip_handler.is_none());
578 assert!(
579 rs.settle_payment(&payload, &req, None)
580 .await
581 .unwrap()
582 .is_success()
583 );
584 assert_eq!(mock.verifies.load(Ordering::SeqCst), 1);
585 assert_eq!(mock.settles.load(Ordering::SeqCst), 1);
586 }
587
588 #[test]
589 fn find_matching_delegates_to_core() {
590 let rs = ResourceServer::new(mock_ok());
591 let payload = sample_payload();
592 let available = [payload.accepted.clone()];
593 assert!(
594 rs.find_matching_requirements(&available, &payload)
595 .is_some()
596 );
597 }
598
599 struct AbortBeforeVerify;
600 impl ResourceServerHooks for AbortBeforeVerify {
601 fn before_verify<'a>(
602 &'a self,
603 _: &PaymentHookContext,
604 ) -> impl Future<Output = BeforeOpDecision<VerifyResponse>> + Send + 'a {
605 std::future::ready(BeforeOpDecision::Abort {
606 reason: "blocked".into(),
607 message: "test".into(),
608 })
609 }
610 }
611
612 #[tokio::test]
613 async fn before_verify_abort() {
614 let rs = ResourceServer::new(mock_ok()).with_hook(AbortBeforeVerify);
615 let payload = sample_payload();
616 let req = payload.accepted.clone();
617 let err = rs.verify_payment(&payload, &req).await.unwrap_err();
618 assert!(matches!(err, FacilitatorError::Aborted { reason, .. } if reason == "blocked"));
619 }
620
621 struct SkipVerify;
622 impl ResourceServerHooks for SkipVerify {
623 fn before_verify<'a>(
624 &'a self,
625 _: &PaymentHookContext,
626 ) -> impl Future<Output = BeforeOpDecision<VerifyResponse>> + Send + 'a {
627 std::future::ready(BeforeOpDecision::Skip {
628 result: VerifyResponse::valid("0xskip"),
629 })
630 }
631 }
632
633 #[tokio::test]
634 async fn before_verify_skip_bypasses_facilitator() {
635 let mock = mock_ok();
636 let rs = ResourceServer::new(Arc::clone(&mock)).with_hook(SkipVerify);
637 let payload = sample_payload();
638 let req = payload.accepted.clone();
639 let out = rs.verify_payment(&payload, &req).await.unwrap();
640 assert!(out.response.is_valid());
641 assert_eq!(mock.verifies.load(Ordering::SeqCst), 0);
642 }
643
644 struct AfterAbort;
645 impl ResourceServerHooks for AfterAbort {
646 fn after_verify<'a>(
647 &'a self,
648 _: &VerifyResultContext,
649 ) -> impl Future<Output = AfterVerifyDecision> + Send + 'a {
650 std::future::ready(AfterVerifyDecision::Abort {
651 reason: "post".into(),
652 message: "nope".into(),
653 })
654 }
655 }
656
657 struct CancelHook(Arc<AtomicUsize>);
658 impl ResourceServerHooks for CancelHook {
659 fn on_verified_payment_canceled<'a>(
660 &'a self,
661 ctx: &'a VerifiedPaymentCanceledContext,
662 ) -> impl Future<Output = ()> + Send + 'a {
663 assert_eq!(ctx.reason, CancelReason::AfterVerifyAborted);
664 self.0.fetch_add(1, Ordering::SeqCst);
665 std::future::ready(())
666 }
667 }
668
669 #[tokio::test]
670 async fn after_verify_abort_fires_cancel() {
671 let cancels = Arc::new(AtomicUsize::new(0));
672 let rs = ResourceServer::new(mock_ok())
673 .with_hook(AfterAbort)
674 .with_hook(CancelHook(Arc::clone(&cancels)));
675 let payload = sample_payload();
676 let req = payload.accepted.clone();
677 let err = rs.verify_payment(&payload, &req).await.unwrap_err();
678 assert!(matches!(err, FacilitatorError::Aborted { reason, .. } if reason == "post"));
679 assert_eq!(cancels.load(Ordering::SeqCst), 1);
680 }
681
682 struct SkipHandlerHook;
683 impl ResourceServerHooks for SkipHandlerHook {
684 fn after_verify<'a>(
685 &'a self,
686 _: &VerifyResultContext,
687 ) -> impl Future<Output = AfterVerifyDecision> + Send + 'a {
688 std::future::ready(AfterVerifyDecision::SkipHandler {
689 response: SkipHandlerDirective::empty().with_body(serde_json::json!({"ok": true})),
690 })
691 }
692 }
693
694 #[tokio::test]
695 async fn after_verify_skip_handler() {
696 let rs = ResourceServer::new(mock_ok()).with_hook(SkipHandlerHook);
697 let payload = sample_payload();
698 let req = payload.accepted.clone();
699 let out = rs.verify_payment(&payload, &req).await.unwrap();
700 assert!(out.skip_handler.is_some());
701 }
702
703 struct RecoverVerify;
704 impl ResourceServerHooks for RecoverVerify {
705 fn on_verify_failure<'a>(
706 &'a self,
707 _: &PaymentHookContext,
708 _: &FacilitatorError,
709 ) -> impl Future<Output = FailureRecovery<VerifyResponse>> + Send + 'a {
710 std::future::ready(FailureRecovery::Recovered(VerifyResponse::valid("0xrec")))
711 }
712 }
713
714 #[tokio::test]
715 async fn verify_failure_recovers() {
716 let mock = Arc::new(MockFacilitator {
717 verifies: AtomicUsize::new(0),
718 settles: AtomicUsize::new(0),
719 fail_verify: true,
720 fail_settle: false,
721 });
722 let rs = ResourceServer::new(mock).with_hook(RecoverVerify);
723 let payload = sample_payload();
724 let req = payload.accepted.clone();
725 assert!(
726 rs.verify_payment(&payload, &req)
727 .await
728 .unwrap()
729 .response
730 .is_valid()
731 );
732 }
733
734 struct CancelCountHook(Arc<AtomicUsize>);
735 impl ResourceServerHooks for CancelCountHook {
736 fn on_verified_payment_canceled<'a>(
737 &'a self,
738 _: &VerifiedPaymentCanceledContext,
739 ) -> impl Future<Output = ()> + Send + 'a {
740 self.0.fetch_add(1, Ordering::SeqCst);
741 std::future::ready(())
742 }
743 }
744
745 #[tokio::test]
746 async fn cancellation_guard_fires_once() {
747 let count = Arc::new(AtomicUsize::new(0));
748 let rs = ResourceServer::new(mock_ok()).with_hook(CancelCountHook(Arc::clone(&count)));
749 let payload = sample_payload();
750 let req = payload.accepted.clone();
751 let guard = rs.cancellation_guard(payload, req);
752 guard
753 .cancel(CancelReason::HandlerFailed, Some("4xx"), Some(400))
754 .await;
755 guard.cancel(CancelReason::HandlerThrew, None, None).await;
756 assert_eq!(count.load(Ordering::SeqCst), 1);
757 assert!(guard.has_fired());
758 }
759}