1use std::fmt::{self, Debug, Formatter};
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12
13use crate::error::ClientError;
14use crate::facilitator::{BoxFuture, FailureRecovery, HookDecision};
15use crate::scheme::{FirstMatch, PaymentCandidate, PaymentPolicy, PaymentSelector, SchemeClient};
16use crate::wire::{PaymentRequired, SettleResponse};
17
18#[derive(Debug, Clone)]
20#[non_exhaustive]
21pub struct PaymentCreationContext {
22 pub payment_required: PaymentRequired,
24}
25
26impl PaymentCreationContext {
27 #[must_use]
29 pub const fn new(payment_required: PaymentRequired) -> Self {
30 Self { payment_required }
31 }
32}
33
34#[derive(Debug, Clone)]
36#[non_exhaustive]
37pub struct CreatedPayment {
38 pub signed_payload: String,
40 pub payment_required: PaymentRequired,
42}
43
44impl CreatedPayment {
45 #[must_use]
47 pub fn new(signed_payload: impl Into<String>, payment_required: PaymentRequired) -> Self {
48 Self {
49 signed_payload: signed_payload.into(),
50 payment_required,
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
62#[non_exhaustive]
63pub struct PaymentResponseContext {
64 pub payment_required: PaymentRequired,
66 pub signed_payload: String,
68 pub settle_response: Option<SettleResponse>,
70 pub corrective_payment_required: Option<PaymentRequired>,
72}
73
74impl PaymentResponseContext {
75 #[must_use]
77 pub fn new(payment_required: PaymentRequired, signed_payload: impl Into<String>) -> Self {
78 Self {
79 payment_required,
80 signed_payload: signed_payload.into(),
81 settle_response: None,
82 corrective_payment_required: None,
83 }
84 }
85
86 #[must_use]
88 pub fn with_settle_response(mut self, settle: SettleResponse) -> Self {
89 self.settle_response = Some(settle);
90 self
91 }
92
93 #[must_use]
95 pub fn with_corrective_payment_required(mut self, required: PaymentRequired) -> Self {
96 self.corrective_payment_required = Some(required);
97 self
98 }
99}
100
101#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
103#[non_exhaustive]
104pub struct PaymentResponseResult {
105 pub recovered: bool,
107}
108
109impl PaymentResponseResult {
110 #[must_use]
112 pub const fn continue_() -> Self {
113 Self { recovered: false }
114 }
115
116 #[must_use]
118 pub const fn recovered() -> Self {
119 Self { recovered: true }
120 }
121}
122
123pub trait ClientHooks: Send + Sync {
127 fn before_payment_creation<'a>(
129 &'a self,
130 _ctx: &'a PaymentCreationContext,
131 ) -> impl Future<Output = HookDecision> + Send + 'a {
132 async { HookDecision::Continue }
133 }
134
135 fn after_payment_creation<'a>(
137 &'a self,
138 _ctx: &'a PaymentCreationContext,
139 _created: &'a CreatedPayment,
140 ) -> impl Future<Output = ()> + Send + 'a {
141 async {}
142 }
143
144 fn on_payment_creation_failure<'a>(
146 &'a self,
147 _ctx: &'a PaymentCreationContext,
148 _error: &'a str,
149 ) -> impl Future<Output = FailureRecovery<CreatedPayment>> + Send + 'a {
150 async { FailureRecovery::Propagate }
151 }
152
153 fn on_payment_response<'a>(
158 &'a self,
159 _ctx: &'a PaymentResponseContext,
160 ) -> impl Future<Output = PaymentResponseResult> + Send + 'a {
161 async { PaymentResponseResult::continue_() }
162 }
163}
164
165pub trait DynClientHooks: Send + Sync {
167 fn before_payment_creation<'a>(
169 &'a self,
170 ctx: &'a PaymentCreationContext,
171 ) -> Pin<Box<dyn Future<Output = HookDecision> + Send + 'a>>;
172
173 fn after_payment_creation<'a>(
175 &'a self,
176 ctx: &'a PaymentCreationContext,
177 created: &'a CreatedPayment,
178 ) -> BoxFuture<'a, ()>;
179
180 fn on_payment_creation_failure<'a>(
182 &'a self,
183 ctx: &'a PaymentCreationContext,
184 error: &'a str,
185 ) -> BoxFuture<'a, FailureRecovery<CreatedPayment>>;
186
187 fn on_payment_response<'a>(
189 &'a self,
190 ctx: &'a PaymentResponseContext,
191 ) -> Pin<Box<dyn Future<Output = PaymentResponseResult> + Send + 'a>>;
192}
193
194impl<T: ClientHooks + ?Sized> DynClientHooks for T {
195 fn before_payment_creation<'a>(
196 &'a self,
197 ctx: &'a PaymentCreationContext,
198 ) -> Pin<Box<dyn Future<Output = HookDecision> + Send + 'a>> {
199 Box::pin(<Self as ClientHooks>::before_payment_creation(self, ctx))
200 }
201
202 fn after_payment_creation<'a>(
203 &'a self,
204 ctx: &'a PaymentCreationContext,
205 created: &'a CreatedPayment,
206 ) -> BoxFuture<'a, ()> {
207 Box::pin(<Self as ClientHooks>::after_payment_creation(
208 self, ctx, created,
209 ))
210 }
211
212 fn on_payment_creation_failure<'a>(
213 &'a self,
214 ctx: &'a PaymentCreationContext,
215 error: &'a str,
216 ) -> BoxFuture<'a, FailureRecovery<CreatedPayment>> {
217 Box::pin(<Self as ClientHooks>::on_payment_creation_failure(
218 self, ctx, error,
219 ))
220 }
221
222 fn on_payment_response<'a>(
223 &'a self,
224 ctx: &'a PaymentResponseContext,
225 ) -> Pin<Box<dyn Future<Output = PaymentResponseResult> + Send + 'a>> {
226 Box::pin(<Self as ClientHooks>::on_payment_response(self, ctx))
227 }
228}
229
230impl Debug for dyn DynClientHooks {
231 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
232 f.write_str("DynClientHooks")
233 }
234}
235
236pub struct PaymentClient<S = FirstMatch> {
240 schemes: Vec<Arc<dyn SchemeClient>>,
241 selector: S,
242 policies: Vec<Arc<dyn PaymentPolicy>>,
243 hooks: Vec<Arc<dyn DynClientHooks>>,
244}
245
246impl PaymentClient<FirstMatch> {
247 #[must_use]
249 pub fn new() -> Self {
250 Self::default()
251 }
252}
253
254impl Default for PaymentClient<FirstMatch> {
255 fn default() -> Self {
256 Self {
257 schemes: Vec::new(),
258 selector: FirstMatch,
259 policies: Vec::new(),
260 hooks: Vec::new(),
261 }
262 }
263}
264
265impl<S> Debug for PaymentClient<S> {
266 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
267 f.debug_struct("PaymentClient")
268 .field("schemes", &self.schemes.len())
269 .field("policies", &self.policies.len())
270 .field("hooks", &self.hooks.len())
271 .finish_non_exhaustive()
272 }
273}
274
275impl<S> PaymentClient<S> {
276 #[must_use]
278 pub fn register(mut self, scheme: impl SchemeClient + 'static) -> Self {
279 self.schemes.push(Arc::new(scheme));
280 self
281 }
282
283 #[must_use]
285 pub fn with_selector<P: PaymentSelector>(self, selector: P) -> PaymentClient<P> {
286 PaymentClient {
287 schemes: self.schemes,
288 selector,
289 policies: self.policies,
290 hooks: self.hooks,
291 }
292 }
293
294 #[must_use]
296 pub fn with_policy(mut self, policy: impl PaymentPolicy + 'static) -> Self {
297 self.policies.push(Arc::new(policy));
298 self
299 }
300
301 #[must_use]
303 pub fn with_hook(mut self, hook: impl ClientHooks + 'static) -> Self {
304 self.hooks.push(Arc::new(hook));
305 self
306 }
307
308 #[must_use]
310 pub fn scheme_count(&self) -> usize {
311 self.schemes.len()
312 }
313
314 #[must_use]
316 pub fn hook_count(&self) -> usize {
317 self.hooks.len()
318 }
319}
320
321impl<S: PaymentSelector> PaymentClient<S> {
322 #[must_use]
324 pub fn candidates(&self, payment_required: &PaymentRequired) -> Vec<PaymentCandidate> {
325 let mut out = Vec::new();
326 for client in &self.schemes {
327 out.extend(client.accept(payment_required));
328 }
329 out
330 }
331
332 pub async fn create_payment(
340 &self,
341 payment_required: &PaymentRequired,
342 ) -> Result<CreatedPayment, ClientError> {
343 let ctx = PaymentCreationContext {
344 payment_required: payment_required.clone(),
345 };
346
347 if let Some(err) = self.run_before_creation(&ctx).await {
348 return Err(err);
349 }
350
351 match self.create_payment_inner(payment_required).await {
352 Ok(created) => {
353 self.run_after_creation(&ctx, &created).await;
354 Ok(created)
355 }
356 Err(err) => self.recover_creation(&ctx, err).await,
357 }
358 }
359
360 async fn run_before_creation(&self, ctx: &PaymentCreationContext) -> Option<ClientError> {
361 for hook in &self.hooks {
362 if let Some(err) =
363 Self::client_error_from_abort(hook.before_payment_creation(ctx).await)
364 {
365 return Some(err);
366 }
367 }
368 None
369 }
370
371 fn client_error_from_abort(decision: HookDecision) -> Option<ClientError> {
372 let HookDecision::Abort { reason, message } = decision else {
373 return None;
374 };
375 let detail = if message.is_empty() {
376 reason
377 } else {
378 format!("{reason}: {message}")
379 };
380 Some(ClientError::Parse(detail))
381 }
382
383 async fn run_after_creation(&self, ctx: &PaymentCreationContext, created: &CreatedPayment) {
384 for hook in &self.hooks {
385 hook.after_payment_creation(ctx, created).await;
386 }
387 }
388
389 async fn recover_creation(
390 &self,
391 ctx: &PaymentCreationContext,
392 err: ClientError,
393 ) -> Result<CreatedPayment, ClientError> {
394 let msg = err.to_string();
395 for hook in &self.hooks {
396 let recovery = hook.on_payment_creation_failure(ctx, &msg).await;
397 if let FailureRecovery::Recovered(created) = recovery {
398 return Ok(created);
399 }
400 }
401 Err(err)
402 }
403
404 async fn create_payment_inner(
405 &self,
406 payment_required: &PaymentRequired,
407 ) -> Result<CreatedPayment, ClientError> {
408 let candidates = self.candidates(payment_required);
409 let mut filtered: Vec<&PaymentCandidate> = candidates.iter().collect();
410 for policy in &self.policies {
411 filtered = policy.apply(filtered);
412 if filtered.is_empty() {
413 return Err(ClientError::NoMatchingPaymentOption);
414 }
415 }
416 let selected = self
417 .selector
418 .select(&filtered)
419 .ok_or(ClientError::NoMatchingPaymentOption)?;
420 let signed_payload = selected.sign().await?;
421 Ok(CreatedPayment::new(
422 signed_payload,
423 payment_required.clone(),
424 ))
425 }
426
427 pub async fn handle_payment_response(
431 &self,
432 ctx: &PaymentResponseContext,
433 ) -> PaymentResponseResult {
434 let mut recovered = false;
435 for hook in &self.hooks {
436 let result = hook.on_payment_response(ctx).await;
437 if result.recovered {
438 recovered = true;
439 }
440 }
441 if recovered {
442 PaymentResponseResult::recovered()
443 } else {
444 PaymentResponseResult::continue_()
445 }
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use std::future::Future;
452
453 use super::*;
454 use crate::chain::ChainId;
455 use crate::scheme::{PaymentCandidateSigner, SchemeId, Sealed};
456 use crate::wire::{PaymentRequirements, ResourceInfo};
457
458 struct StubSigner(String);
459 impl PaymentCandidateSigner for StubSigner {
460 fn sign_payment<'a>(
461 &'a self,
462 ) -> Pin<Box<dyn Future<Output = Result<String, ClientError>> + Send + 'a>> {
463 Box::pin(async move { Ok(self.0.clone()) })
464 }
465 }
466
467 struct StubScheme;
468 impl Sealed for StubScheme {}
469 impl SchemeId for StubScheme {
470 fn namespace(&self) -> &'static str {
471 "eip155"
472 }
473 fn scheme(&self) -> &'static str {
474 "exact"
475 }
476 }
477 impl SchemeClient for StubScheme {
478 fn accept(&self, required: &PaymentRequired) -> Vec<PaymentCandidate> {
479 required
480 .accepts
481 .iter()
482 .map(|r| PaymentCandidate {
483 chain_id: r.network.clone(),
484 asset: r.asset.clone(),
485 amount: r.amount.clone(),
486 scheme: r.scheme.clone(),
487 pay_to: r.pay_to.clone(),
488 signer: Box::new(StubSigner("c2lnbmVk".into())),
489 })
490 .collect()
491 }
492 }
493
494 struct AbortHook;
495 impl ClientHooks for AbortHook {
496 fn before_payment_creation<'a>(
497 &'a self,
498 _: &PaymentCreationContext,
499 ) -> impl Future<Output = HookDecision> + Send + 'a {
500 std::future::ready(HookDecision::Abort {
501 reason: "nope".into(),
502 message: String::new(),
503 })
504 }
505 }
506
507 struct RecoverHook;
508 impl ClientHooks for RecoverHook {
509 fn on_payment_response<'a>(
510 &'a self,
511 _: &PaymentResponseContext,
512 ) -> impl Future<Output = PaymentResponseResult> + Send + 'a {
513 std::future::ready(PaymentResponseResult::recovered())
514 }
515 }
516
517 fn sample_required() -> PaymentRequired {
518 let req = PaymentRequirements::new(
519 "exact".into(),
520 "eip155:1".parse::<ChainId>().unwrap(),
521 "1".into(),
522 "0xa".into(),
523 "0xb".into(),
524 60,
525 );
526 PaymentRequired::new(ResourceInfo::new("https://example.com")).with_accepts(vec![req])
527 }
528
529 #[tokio::test]
530 async fn create_payment_signs() {
531 let client = PaymentClient::new().register(StubScheme);
532 let created = client.create_payment(&sample_required()).await.unwrap();
533 assert_eq!(created.signed_payload, "c2lnbmVk");
534 }
535
536 #[tokio::test]
537 async fn before_hook_aborts() {
538 let client = PaymentClient::new()
539 .register(StubScheme)
540 .with_hook(AbortHook);
541 let err = client.create_payment(&sample_required()).await.unwrap_err();
542 assert!(matches!(err, ClientError::Parse(s) if s.contains("nope")));
543 }
544
545 #[tokio::test]
546 async fn payment_response_hook_recovers() {
547 let client = PaymentClient::new().with_hook(RecoverHook);
548 let ctx = PaymentResponseContext::new(sample_required(), "x")
549 .with_corrective_payment_required(sample_required());
550 let result = client.handle_payment_response(&ctx).await;
551 assert!(result.recovered);
552 }
553
554 #[tokio::test]
555 async fn no_schemes_yields_no_match() {
556 let client = PaymentClient::new();
557 let err = client.create_payment(&sample_required()).await.unwrap_err();
558 assert!(matches!(err, ClientError::NoMatchingPaymentOption));
559 }
560}