Skip to main content

r402_core/
hooks.rs

1//! Lifecycle hooks for facilitator verify / settle operations.
2//!
3//! A [`HookedFacilitator`] wraps any [`Facilitator`] and runs registered
4//! [`FacilitatorHooks`] at three lifecycle points:
5//!
6//! 1. **Before** — inspect or abort the operation.
7//! 2. **After** — observe a successful result.
8//! 3. **On failure** — observe or recover from an error.
9//!
10//! Hook methods use AFIT for zero-cost static dispatch when the hook type is
11//! statically known. For heterogeneous lists (e.g. a registry collecting
12//! multiple hook implementations) use the [`DynFacilitatorHooks`] erasure.
13
14use std::fmt::{self, Debug, Formatter};
15use std::future::Future;
16use std::pin::Pin;
17
18use crate::error::FacilitatorError;
19use crate::facilitator::{BoxFuture, Facilitator};
20use crate::wire::{
21    SettleRequest, SettleResponse, SupportedResponse, VerifyRequest, VerifyResponse,
22};
23
24/// Decision returned by "before" hooks to control whether the operation
25/// proceeds.
26#[derive(Debug, Clone)]
27#[non_exhaustive]
28pub enum HookDecision {
29    /// Continue execution normally.
30    Continue,
31    /// Abort with a structured reason + message.
32    Abort {
33        /// Machine-readable reason for aborting.
34        reason: String,
35        /// Human-readable description.
36        message: String,
37    },
38}
39
40/// Outcome returned by "on failure" hooks, indicating whether recovery
41/// happened.
42#[derive(Debug)]
43#[non_exhaustive]
44pub enum FailureRecovery<T> {
45    /// No recovery — propagate the original error.
46    Propagate,
47    /// The hook produced a substitute success result.
48    Recovered(T),
49}
50
51/// Context passed to verify-related hooks.
52#[derive(Clone)]
53#[non_exhaustive]
54pub struct VerifyContext {
55    /// The incoming verify request.
56    pub request: VerifyRequest,
57}
58
59impl Debug for VerifyContext {
60    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
61        f.debug_struct("VerifyContext").finish_non_exhaustive()
62    }
63}
64
65/// Context passed to settle-related hooks.
66#[derive(Clone)]
67#[non_exhaustive]
68pub struct SettleContext {
69    /// The incoming settle request.
70    pub request: SettleRequest,
71}
72
73impl Debug for SettleContext {
74    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
75        f.debug_struct("SettleContext").finish_non_exhaustive()
76    }
77}
78
79/// Lifecycle hooks for facilitator verify and settle operations.
80///
81/// All methods default to a no-op — implementers override only what they
82/// need. The trait uses AFIT for static dispatch; see
83/// [`DynFacilitatorHooks`] for the object-safe erasure.
84pub trait FacilitatorHooks: Send + Sync {
85    /// Runs before every `verify` call.
86    fn before_verify<'a>(
87        &'a self,
88        _ctx: &'a VerifyContext,
89    ) -> impl Future<Output = HookDecision> + Send + 'a {
90        async { HookDecision::Continue }
91    }
92
93    /// Runs after every successful `verify` call.
94    fn after_verify<'a>(
95        &'a self,
96        _ctx: &'a VerifyContext,
97        _response: &'a VerifyResponse,
98    ) -> impl Future<Output = ()> + Send + 'a {
99        async {}
100    }
101
102    /// Runs when a `verify` call returns an error.
103    fn on_verify_failure<'a>(
104        &'a self,
105        _ctx: &'a VerifyContext,
106        _error: &'a FacilitatorError,
107    ) -> impl Future<Output = FailureRecovery<VerifyResponse>> + Send + 'a {
108        async { FailureRecovery::Propagate }
109    }
110
111    /// Runs before every `settle` call.
112    fn before_settle<'a>(
113        &'a self,
114        _ctx: &'a SettleContext,
115    ) -> impl Future<Output = HookDecision> + Send + 'a {
116        async { HookDecision::Continue }
117    }
118
119    /// Runs after every successful `settle` call.
120    fn after_settle<'a>(
121        &'a self,
122        _ctx: &'a SettleContext,
123        _response: &'a SettleResponse,
124    ) -> impl Future<Output = ()> + Send + 'a {
125        async {}
126    }
127
128    /// Runs when a `settle` call returns an error.
129    fn on_settle_failure<'a>(
130        &'a self,
131        _ctx: &'a SettleContext,
132        _error: &'a FacilitatorError,
133    ) -> impl Future<Output = FailureRecovery<SettleResponse>> + Send + 'a {
134        async { FailureRecovery::Propagate }
135    }
136}
137
138/// Object-safe erasure of [`FacilitatorHooks`].
139pub trait DynFacilitatorHooks: Send + Sync {
140    /// See [`FacilitatorHooks::before_verify`].
141    fn before_verify<'a>(
142        &'a self,
143        ctx: &'a VerifyContext,
144    ) -> Pin<Box<dyn Future<Output = HookDecision> + Send + 'a>>;
145
146    /// See [`FacilitatorHooks::after_verify`].
147    fn after_verify<'a>(
148        &'a self,
149        ctx: &'a VerifyContext,
150        response: &'a VerifyResponse,
151    ) -> BoxFuture<'a, ()>;
152
153    /// See [`FacilitatorHooks::on_verify_failure`].
154    fn on_verify_failure<'a>(
155        &'a self,
156        ctx: &'a VerifyContext,
157        error: &'a FacilitatorError,
158    ) -> BoxFuture<'a, FailureRecovery<VerifyResponse>>;
159
160    /// See [`FacilitatorHooks::before_settle`].
161    fn before_settle<'a>(&'a self, ctx: &'a SettleContext) -> BoxFuture<'a, HookDecision>;
162
163    /// See [`FacilitatorHooks::after_settle`].
164    fn after_settle<'a>(
165        &'a self,
166        ctx: &'a SettleContext,
167        response: &'a SettleResponse,
168    ) -> BoxFuture<'a, ()>;
169
170    /// See [`FacilitatorHooks::on_settle_failure`].
171    fn on_settle_failure<'a>(
172        &'a self,
173        ctx: &'a SettleContext,
174        error: &'a FacilitatorError,
175    ) -> BoxFuture<'a, FailureRecovery<SettleResponse>>;
176}
177
178impl<T: FacilitatorHooks + ?Sized> DynFacilitatorHooks for T {
179    fn before_verify<'a>(&'a self, ctx: &'a VerifyContext) -> BoxFuture<'a, HookDecision> {
180        Box::pin(<Self as FacilitatorHooks>::before_verify(self, ctx))
181    }
182
183    fn after_verify<'a>(
184        &'a self,
185        ctx: &'a VerifyContext,
186        response: &'a VerifyResponse,
187    ) -> BoxFuture<'a, ()> {
188        Box::pin(<Self as FacilitatorHooks>::after_verify(
189            self, ctx, response,
190        ))
191    }
192
193    fn on_verify_failure<'a>(
194        &'a self,
195        ctx: &'a VerifyContext,
196        error: &'a FacilitatorError,
197    ) -> BoxFuture<'a, FailureRecovery<VerifyResponse>> {
198        Box::pin(<Self as FacilitatorHooks>::on_verify_failure(
199            self, ctx, error,
200        ))
201    }
202
203    fn before_settle<'a>(&'a self, ctx: &'a SettleContext) -> BoxFuture<'a, HookDecision> {
204        Box::pin(<Self as FacilitatorHooks>::before_settle(self, ctx))
205    }
206
207    fn after_settle<'a>(
208        &'a self,
209        ctx: &'a SettleContext,
210        response: &'a SettleResponse,
211    ) -> BoxFuture<'a, ()> {
212        Box::pin(<Self as FacilitatorHooks>::after_settle(
213            self, ctx, response,
214        ))
215    }
216
217    fn on_settle_failure<'a>(
218        &'a self,
219        ctx: &'a SettleContext,
220        error: &'a FacilitatorError,
221    ) -> BoxFuture<'a, FailureRecovery<SettleResponse>> {
222        Box::pin(<Self as FacilitatorHooks>::on_settle_failure(
223            self, ctx, error,
224        ))
225    }
226}
227
228/// Facilitator decorator that runs registered hooks around verify and settle.
229///
230/// Hook invocation order:
231///
232/// - **Before** hooks run in registration order; first `Abort` wins.
233/// - **After** hooks run in registration order; errors are silently dropped.
234/// - **On-failure** hooks run in registration order; first `Recovered` wins.
235pub struct HookedFacilitator<F> {
236    inner: F,
237    hooks: Vec<Box<dyn DynFacilitatorHooks>>,
238}
239
240impl<F: Debug> Debug for HookedFacilitator<F> {
241    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
242        f.debug_struct("HookedFacilitator")
243            .field("inner", &self.inner)
244            .field("hooks", &format_args!("[{} hooks]", self.hooks.len()))
245            .finish()
246    }
247}
248
249impl<F> HookedFacilitator<F> {
250    /// Wraps `inner` with an empty hook list.
251    pub const fn new(inner: F) -> Self {
252        Self {
253            inner,
254            hooks: Vec::new(),
255        }
256    }
257
258    /// Registers a hook. Returns `self` so builder-style chaining works.
259    #[must_use]
260    pub fn with_hook(mut self, hook: impl FacilitatorHooks + 'static) -> Self {
261        self.hooks.push(Box::new(hook));
262        self
263    }
264
265    /// Registers a hook after construction.
266    pub fn add_hook(&mut self, hook: impl FacilitatorHooks + 'static) {
267        self.hooks.push(Box::new(hook));
268    }
269
270    /// Number of registered hooks.
271    #[must_use]
272    pub fn hook_count(&self) -> usize {
273        self.hooks.len()
274    }
275
276    /// Returns a reference to the inner facilitator.
277    #[must_use]
278    pub const fn inner(&self) -> &F {
279        &self.inner
280    }
281}
282
283impl<F: Sync> HookedFacilitator<F> {
284    async fn run_before_verify(&self, ctx: &VerifyContext) -> Result<(), FacilitatorError> {
285        for hook in &self.hooks {
286            if let HookDecision::Abort { reason, message } = hook.before_verify(ctx).await {
287                return Err(FacilitatorError::Aborted { reason, message });
288            }
289        }
290        Ok(())
291    }
292
293    async fn run_after_verify(&self, ctx: &VerifyContext, response: &VerifyResponse) {
294        for hook in &self.hooks {
295            hook.after_verify(ctx, response).await;
296        }
297    }
298
299    async fn run_on_verify_failure(
300        &self,
301        ctx: &VerifyContext,
302        error: &FacilitatorError,
303    ) -> Option<VerifyResponse> {
304        for hook in &self.hooks {
305            if let FailureRecovery::Recovered(response) = hook.on_verify_failure(ctx, error).await {
306                return Some(response);
307            }
308        }
309        None
310    }
311
312    async fn run_before_settle(&self, ctx: &SettleContext) -> Result<(), FacilitatorError> {
313        for hook in &self.hooks {
314            if let HookDecision::Abort { reason, message } = hook.before_settle(ctx).await {
315                return Err(FacilitatorError::Aborted { reason, message });
316            }
317        }
318        Ok(())
319    }
320
321    async fn run_after_settle(&self, ctx: &SettleContext, response: &SettleResponse) {
322        for hook in &self.hooks {
323            hook.after_settle(ctx, response).await;
324        }
325    }
326
327    async fn run_on_settle_failure(
328        &self,
329        ctx: &SettleContext,
330        error: &FacilitatorError,
331    ) -> Option<SettleResponse> {
332        for hook in &self.hooks {
333            if let FailureRecovery::Recovered(response) = hook.on_settle_failure(ctx, error).await {
334                return Some(response);
335            }
336        }
337        None
338    }
339}
340
341impl<F: Facilitator + Sync> Facilitator for HookedFacilitator<F> {
342    async fn verify(&self, request: VerifyRequest) -> Result<VerifyResponse, FacilitatorError> {
343        let ctx = VerifyContext {
344            request: request.clone(),
345        };
346        self.run_before_verify(&ctx).await?;
347        match self.inner.verify(request).await {
348            Ok(response) => {
349                self.run_after_verify(&ctx, &response).await;
350                Ok(response)
351            }
352            Err(error) => {
353                let recovered = self.run_on_verify_failure(&ctx, &error).await;
354                recovered.ok_or(error)
355            }
356        }
357    }
358
359    async fn settle(&self, request: SettleRequest) -> Result<SettleResponse, FacilitatorError> {
360        let ctx = SettleContext {
361            request: request.clone(),
362        };
363        self.run_before_settle(&ctx).await?;
364        match self.inner.settle(request).await {
365            Ok(response) => {
366                self.run_after_settle(&ctx, &response).await;
367                Ok(response)
368            }
369            Err(error) => {
370                let recovered = self.run_on_settle_failure(&ctx, &error).await;
371                recovered.ok_or(error)
372            }
373        }
374    }
375
376    async fn supported(&self) -> Result<SupportedResponse, FacilitatorError> {
377        self.inner.supported().await
378    }
379}
380
381#[cfg(test)]
382#[allow(
383    clippy::excessive_nesting,
384    reason = "mock facilitator impls inherently nest async fn bodies inside impl-in-fn"
385)]
386mod tests {
387    use std::sync::atomic::{AtomicUsize, Ordering};
388
389    use super::*;
390    use crate::error_reason::ErrorReason;
391    use crate::wire::Extensions;
392
393    struct MockFacilitator {
394        fail: bool,
395    }
396
397    impl MockFacilitator {
398        fn ok() -> Self {
399            Self { fail: false }
400        }
401        fn failing() -> Self {
402            Self { fail: true }
403        }
404    }
405
406    impl Facilitator for MockFacilitator {
407        fn verify(
408            &self,
409            _request: VerifyRequest,
410        ) -> impl Future<Output = Result<VerifyResponse, FacilitatorError>> + Send {
411            let result = if self.fail {
412                Err(FacilitatorError::Onchain("mock".into()))
413            } else {
414                Ok(VerifyResponse::valid("0xPAYER"))
415            };
416            std::future::ready(result)
417        }
418
419        fn settle(
420            &self,
421            _request: SettleRequest,
422        ) -> impl Future<Output = Result<SettleResponse, FacilitatorError>> + Send {
423            let result = if self.fail {
424                Err(FacilitatorError::Onchain("mock".into()))
425            } else {
426                Ok(SettleResponse::Success {
427                    payer: "0xPAYER".into(),
428                    transaction: "0xTX".into(),
429                    network: "eip155:1".into(),
430                    amount: None,
431                    extensions: Extensions::new(),
432                })
433            };
434            std::future::ready(result)
435        }
436
437        fn supported(
438            &self,
439        ) -> impl Future<Output = Result<SupportedResponse, FacilitatorError>> + Send {
440            std::future::ready(Ok(SupportedResponse::default()))
441        }
442    }
443
444    struct AbortVerifyHook;
445    impl FacilitatorHooks for AbortVerifyHook {
446        fn before_verify<'a>(
447            &'a self,
448            _: &VerifyContext,
449        ) -> impl Future<Output = HookDecision> + Send + 'a {
450            std::future::ready(HookDecision::Abort {
451                reason: "blocked".into(),
452                message: "test".into(),
453            })
454        }
455    }
456
457    struct AbortSettleHook;
458    impl FacilitatorHooks for AbortSettleHook {
459        fn before_settle<'a>(
460            &'a self,
461            _: &SettleContext,
462        ) -> impl Future<Output = HookDecision> + Send + 'a {
463            std::future::ready(HookDecision::Abort {
464                reason: "blocked".into(),
465                message: "test".into(),
466            })
467        }
468    }
469
470    struct RecoverVerifyHook;
471    impl FacilitatorHooks for RecoverVerifyHook {
472        fn on_verify_failure<'a>(
473            &'a self,
474            _: &VerifyContext,
475            _: &FacilitatorError,
476        ) -> impl Future<Output = FailureRecovery<VerifyResponse>> + Send + 'a {
477            std::future::ready(FailureRecovery::Recovered(VerifyResponse::valid("0xREC")))
478        }
479    }
480
481    struct RecoverSettleHook;
482    impl FacilitatorHooks for RecoverSettleHook {
483        fn on_settle_failure<'a>(
484            &'a self,
485            _: &SettleContext,
486            _: &FacilitatorError,
487        ) -> impl Future<Output = FailureRecovery<SettleResponse>> + Send + 'a {
488            std::future::ready(FailureRecovery::Recovered(SettleResponse::Success {
489                payer: "0xREC".into(),
490                transaction: "0xREC_TX".into(),
491                network: "eip155:1".into(),
492                amount: None,
493                extensions: Extensions::new(),
494            }))
495        }
496    }
497
498    struct NoopHook;
499    impl FacilitatorHooks for NoopHook {}
500
501    struct SecondAbortHook(&'static AtomicUsize);
502    impl FacilitatorHooks for SecondAbortHook {
503        fn before_verify<'a>(
504            &'a self,
505            _: &VerifyContext,
506        ) -> impl Future<Output = HookDecision> + Send + 'a {
507            let _ = self.0.fetch_add(1, Ordering::Relaxed);
508            std::future::ready(HookDecision::Abort {
509                reason: "second".into(),
510                message: String::new(),
511            })
512        }
513    }
514
515    fn dummy_verify() -> VerifyRequest {
516        serde_json::json!({}).into()
517    }
518    fn dummy_settle() -> SettleRequest {
519        serde_json::json!({}).into()
520    }
521
522    #[tokio::test]
523    async fn verify_no_hooks_passes_through() {
524        let hooked = HookedFacilitator::new(MockFacilitator::ok());
525        assert_eq!(hooked.hook_count(), 0);
526        let response = hooked.verify(dummy_verify()).await.unwrap();
527        assert!(response.is_valid());
528    }
529
530    #[tokio::test]
531    async fn verify_before_hook_aborts() {
532        let hooked = HookedFacilitator::new(MockFacilitator::ok()).with_hook(AbortVerifyHook);
533        let err = hooked.verify(dummy_verify()).await.unwrap_err();
534        assert!(matches!(err, FacilitatorError::Aborted { reason, .. } if reason == "blocked"));
535    }
536
537    #[tokio::test]
538    async fn verify_failure_hook_recovers() {
539        let hooked =
540            HookedFacilitator::new(MockFacilitator::failing()).with_hook(RecoverVerifyHook);
541        assert!(hooked.verify(dummy_verify()).await.unwrap().is_valid());
542    }
543
544    #[tokio::test]
545    async fn verify_failure_hook_propagates_by_default() {
546        let hooked = HookedFacilitator::new(MockFacilitator::failing()).with_hook(NoopHook);
547        assert!(hooked.verify(dummy_verify()).await.is_err());
548    }
549
550    #[tokio::test]
551    async fn settle_before_hook_aborts() {
552        let hooked = HookedFacilitator::new(MockFacilitator::ok()).with_hook(AbortSettleHook);
553        let err = hooked.settle(dummy_settle()).await.unwrap_err();
554        assert!(matches!(err, FacilitatorError::Aborted { reason, .. } if reason == "blocked"));
555    }
556
557    #[tokio::test]
558    async fn settle_success_passes_through() {
559        let hooked = HookedFacilitator::new(MockFacilitator::ok());
560        assert!(hooked.settle(dummy_settle()).await.unwrap().is_success());
561    }
562
563    #[tokio::test]
564    async fn settle_failure_hook_recovers() {
565        let hooked =
566            HookedFacilitator::new(MockFacilitator::failing()).with_hook(RecoverSettleHook);
567        assert!(hooked.settle(dummy_settle()).await.unwrap().is_success());
568    }
569
570    #[tokio::test]
571    async fn first_abort_wins_remaining_skipped() {
572        static SECOND_CALLS: AtomicUsize = AtomicUsize::new(0);
573        SECOND_CALLS.store(0, Ordering::Relaxed);
574        let hooked = HookedFacilitator::new(MockFacilitator::ok())
575            .with_hook(AbortVerifyHook)
576            .with_hook(SecondAbortHook(&SECOND_CALLS));
577        let err = hooked.verify(dummy_verify()).await.unwrap_err();
578        assert!(matches!(err, FacilitatorError::Aborted { reason, .. } if reason == "blocked"));
579        assert_eq!(SECOND_CALLS.load(Ordering::Relaxed), 0);
580    }
581
582    #[tokio::test]
583    async fn add_hook_dynamic() {
584        let mut hooked = HookedFacilitator::new(MockFacilitator::ok());
585        assert_eq!(hooked.hook_count(), 0);
586        hooked.add_hook(NoopHook);
587        assert_eq!(hooked.hook_count(), 1);
588        assert!(hooked.verify(dummy_verify()).await.unwrap().is_valid());
589    }
590
591    #[tokio::test]
592    async fn supported_delegates_to_inner() {
593        let hooked = HookedFacilitator::new(MockFacilitator::ok());
594        let response = hooked.supported().await.unwrap();
595        assert!(response.kinds.is_empty());
596        assert!(response.signers.is_empty());
597    }
598
599    #[tokio::test]
600    async fn verify_invalid_response_with_reason_round_trip() {
601        struct Invalid;
602        impl Facilitator for Invalid {
603            fn verify(
604                &self,
605                _r: VerifyRequest,
606            ) -> impl Future<Output = Result<VerifyResponse, FacilitatorError>> + Send {
607                std::future::ready(Ok(VerifyResponse::invalid(
608                    None,
609                    ErrorReason::InvalidPayload,
610                )))
611            }
612            fn settle(
613                &self,
614                _r: SettleRequest,
615            ) -> impl Future<Output = Result<SettleResponse, FacilitatorError>> + Send {
616                std::future::ready(Err(FacilitatorError::Onchain("unreachable".into())))
617            }
618            fn supported(
619                &self,
620            ) -> impl Future<Output = Result<SupportedResponse, FacilitatorError>> + Send
621            {
622                std::future::ready(Ok(SupportedResponse::default()))
623            }
624        }
625        let hooked = HookedFacilitator::new(Invalid);
626        let response = hooked.verify(dummy_verify()).await.unwrap();
627        match response {
628            VerifyResponse::Invalid { reason, .. } => {
629                assert_eq!(reason, ErrorReason::InvalidPayload);
630            }
631            VerifyResponse::Valid { .. } => panic!("expected invalid"),
632        }
633    }
634}