1use 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#[derive(Debug, Clone)]
27#[non_exhaustive]
28pub enum HookDecision {
29 Continue,
31 Abort {
33 reason: String,
35 message: String,
37 },
38}
39
40#[derive(Debug)]
43#[non_exhaustive]
44pub enum FailureRecovery<T> {
45 Propagate,
47 Recovered(T),
49}
50
51#[derive(Clone)]
53#[non_exhaustive]
54pub struct VerifyContext {
55 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#[derive(Clone)]
67#[non_exhaustive]
68pub struct SettleContext {
69 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
79pub trait FacilitatorHooks: Send + Sync {
85 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 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 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 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 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 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
138pub trait DynFacilitatorHooks: Send + Sync {
140 fn before_verify<'a>(
142 &'a self,
143 ctx: &'a VerifyContext,
144 ) -> Pin<Box<dyn Future<Output = HookDecision> + Send + 'a>>;
145
146 fn after_verify<'a>(
148 &'a self,
149 ctx: &'a VerifyContext,
150 response: &'a VerifyResponse,
151 ) -> BoxFuture<'a, ()>;
152
153 fn on_verify_failure<'a>(
155 &'a self,
156 ctx: &'a VerifyContext,
157 error: &'a FacilitatorError,
158 ) -> BoxFuture<'a, FailureRecovery<VerifyResponse>>;
159
160 fn before_settle<'a>(&'a self, ctx: &'a SettleContext) -> BoxFuture<'a, HookDecision>;
162
163 fn after_settle<'a>(
165 &'a self,
166 ctx: &'a SettleContext,
167 response: &'a SettleResponse,
168 ) -> BoxFuture<'a, ()>;
169
170 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
228pub 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 pub const fn new(inner: F) -> Self {
252 Self {
253 inner,
254 hooks: Vec::new(),
255 }
256 }
257
258 #[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 pub fn add_hook(&mut self, hook: impl FacilitatorHooks + 'static) {
267 self.hooks.push(Box::new(hook));
268 }
269
270 #[must_use]
272 pub fn hook_count(&self) -> usize {
273 self.hooks.len()
274 }
275
276 #[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}