Skip to main content

r402_core/resource_server/
mod.rs

1//! Resource-server orchestration shared by HTTP and MCP transports.
2//!
3//! Mirrors the official Go/TS `X402ResourceServer` **V2** surface used by
4//! transports: match requirements, verify, settle, and lifecycle hooks
5//! including verified-payment cancellation.
6//!
7//! This type does **not** own pricing or route maps; scheme handlers live in
8//! [`crate::scheme::SchemeRegistry`] (or a remote facilitator). The resource
9//! server only sequences wire construction and hooks against a [`Facilitator`].
10
11mod 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/// Successful verify path outcome (after hooks).
35#[derive(Debug, Clone)]
36#[non_exhaustive]
37pub struct VerifyPaymentOutcome {
38    /// Facilitator (or skipped/recovered) verify response.
39    pub response: VerifyResponse,
40    /// When set, the transport must skip the resource handler and settle inline.
41    pub skip_handler: Option<SkipHandlerDirective>,
42}
43
44/// Server-side payment orchestrator (official `X402ResourceServer`, V2-only).
45pub 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    /// Creates a resource server over any [`Facilitator`].
69    #[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    /// Creates from an already-erased facilitator handle.
82    #[must_use]
83    pub fn from_dyn(facilitator: Arc<dyn DynFacilitator>) -> Self {
84        Self {
85            facilitator,
86            hooks: Vec::new(),
87        }
88    }
89
90    /// Registers a lifecycle hook. Returns `self` for builder chaining.
91    #[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    /// Registers a lifecycle hook after construction.
98    pub fn add_hook(&mut self, hook: impl ResourceServerHooks + 'static) {
99        self.hooks.push(Arc::new(hook));
100    }
101
102    /// Number of registered resource-server hooks.
103    #[must_use]
104    pub fn hook_count(&self) -> usize {
105        self.hooks.len()
106    }
107
108    /// Returns a clone of the inner facilitator handle.
109    #[must_use]
110    pub fn facilitator(&self) -> Arc<dyn DynFacilitator> {
111        Arc::clone(&self.facilitator)
112    }
113
114    /// Official `FindMatchingRequirements`.
115    ///
116    /// Method form matches the Go `X402ResourceServer` surface even though
117    /// matching is pure over the arguments.
118    #[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    /// Official `VerifyPayment`: before hooks → facilitator → after hooks.
132    ///
133    /// # Errors
134    ///
135    /// Propagates facilitator errors (unless recovered), before-hook aborts,
136    /// and after-verify aborts (`FacilitatorError::Aborted`).
137    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        // before_verify: first Abort / Skip wins
148        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        // Official Go: isValid:false is a hard verify failure that runs
170        // onVerifyFailure (security: reachable facilitator ≠ payment valid).
171        // Unrecovered invalids skip after_verify.
172        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                    // Last SkipHandler wins (matches Go).
203                    skip_handler = Some(directive);
204                }
205            }
206        }
207
208        Ok(VerifyPaymentOutcome {
209            response,
210            skip_handler,
211        })
212    }
213
214    /// Official `SettlePayment`: optional amount overrides → before hooks →
215    /// facilitator → after hooks.
216    ///
217    /// `overrides` applies partial settlement (upto): atomic / percent / dollar
218    /// amounts are resolved against `requirements.amount` before hooks see the
219    /// payment context (matches Go `SettlePaymentWithExtensions`).
220    ///
221    /// # Errors
222    ///
223    /// Propagates facilitator / abort / invalid-override errors unless a
224    /// failure hook recovers.
225    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        // Official: afterSettle fires for settle results returned as Ok
259        // (including unsuccessful settle responses from the facilitator).
260        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    /// Fires `on_verified_payment_canceled` for every registered hook.
272    ///
273    /// Transports call this when a handler fails after a successful verify
274    /// (or when an after-verify abort already did — safe to call again via
275    /// [`CancellationGuard`]).
276    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    /// Builds a one-shot cancellation dispatcher (first call wins).
302    #[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/// Ensures `on_verified_payment_canceled` runs at most once for a payment.
374#[derive(Debug)]
375pub struct CancellationGuard {
376    server: ResourceServer,
377    payload: WirePaymentPayload,
378    requirements: PaymentRequirements,
379    fired: AtomicBool,
380}
381
382impl CancellationGuard {
383    /// Fires cancel hooks if not already fired.
384    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    /// Returns `true` when cancel has already been dispatched.
409    #[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
445/// Applies official settlement overrides onto a clone of `requirements`.
446fn 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}