use crate::error::MppError;
use crate::protocol::core::accept_payment::{self, Entry};
use crate::protocol::core::{PaymentChallenge, PaymentCredential};
use reqwest::header::HeaderMap;
use reqwest::Url;
use std::future::Future;
#[derive(Clone, Debug)]
pub struct PaymentContext {
pub url: Url,
pub headers: HeaderMap,
}
pub trait PaymentProvider: Clone + Send + Sync {
fn supports(&self, method: &str, intent: &str) -> bool;
fn select_challenge<'a>(
&self,
challenges: &[&'a PaymentChallenge],
) -> Option<&'a PaymentChallenge> {
challenges.first().copied()
}
fn pay(
&self,
challenge: &PaymentChallenge,
) -> impl Future<Output = Result<PaymentCredential, MppError>> + Send;
fn pay_with_context(
&self,
challenge: &PaymentChallenge,
context: PaymentContext,
) -> impl Future<Output = Result<PaymentCredential, MppError>> + Send {
let _ = context;
self.pay(challenge)
}
fn prepare_http_payment_challenge(
&self,
challenge: &PaymentChallenge,
context: PaymentContext,
) -> impl Future<Output = Result<Option<PaymentChallenge>, MppError>> + Send {
let challenge = challenge.clone();
async move {
let _ = context;
Ok(Some(challenge))
}
}
fn prepare_application_websocket_challenge(
&self,
challenge: &PaymentChallenge,
context: PaymentContext,
) -> impl Future<Output = Result<PaymentChallenge, MppError>> + Send {
let challenge = challenge.clone();
async move {
let _ = context;
Ok(challenge)
}
}
fn commit_payment(
&self,
challenge: &PaymentChallenge,
credential: &PaymentCredential,
) -> impl Future<Output = Result<(), MppError>> + Send {
let _ = (challenge, credential);
async { Ok(()) }
}
fn rollback_payment(
&self,
challenge: &PaymentChallenge,
credential: &PaymentCredential,
) -> impl Future<Output = Result<(), MppError>> + Send {
let _ = (challenge, credential);
async { Ok(()) }
}
fn invalidate_payment(
&self,
challenge: &PaymentChallenge,
credential: &PaymentCredential,
) -> impl Future<Output = Result<(), MppError>> + Send {
self.rollback_payment(challenge, credential)
}
fn abandon_payment(&self, challenge: &PaymentChallenge, credential: &PaymentCredential) {
let _ = (challenge, credential);
}
fn accept_payment_header(&self) -> Option<String> {
None
}
}
pub struct PendingPayment<P: PaymentProvider> {
provider: P,
challenge: PaymentChallenge,
credential: PaymentCredential,
active: bool,
}
impl<P: PaymentProvider> PendingPayment<P> {
pub fn new(provider: P, challenge: PaymentChallenge, credential: PaymentCredential) -> Self {
Self {
provider,
challenge,
credential,
active: true,
}
}
pub fn challenge(&self) -> &PaymentChallenge {
&self.challenge
}
pub fn credential(&self) -> &PaymentCredential {
&self.credential
}
pub async fn commit(mut self) -> Result<(), MppError> {
self.provider
.commit_payment(&self.challenge, &self.credential)
.await?;
self.active = false;
Ok(())
}
pub async fn rollback(mut self) -> Result<(), MppError> {
self.provider
.rollback_payment(&self.challenge, &self.credential)
.await?;
self.active = false;
Ok(())
}
pub async fn invalidate(mut self) -> Result<(), MppError> {
self.provider
.invalidate_payment(&self.challenge, &self.credential)
.await?;
self.active = false;
Ok(())
}
}
impl<P: PaymentProvider> Drop for PendingPayment<P> {
fn drop(&mut self) {
if self.active {
self.provider
.abandon_payment(&self.challenge, &self.credential);
}
}
}
#[derive(Clone)]
pub struct MultiProvider {
providers: Vec<Box<dyn DynPaymentProvider>>,
}
impl MultiProvider {
pub fn new() -> Self {
Self {
providers: Vec::new(),
}
}
pub fn with<P: PaymentProvider + 'static>(mut self, provider: P) -> Self {
self.providers.push(Box::new(provider));
self
}
pub fn add<P: PaymentProvider + 'static>(&mut self, provider: P) -> &mut Self {
self.providers.push(Box::new(provider));
self
}
pub fn has_support(&self, method: &str, intent: &str) -> bool {
self.providers
.iter()
.any(|p| p.dyn_supports(method, intent))
}
}
impl Default for MultiProvider {
fn default() -> Self {
Self::new()
}
}
impl PaymentProvider for MultiProvider {
fn supports(&self, method: &str, intent: &str) -> bool {
self.has_support(method, intent)
}
fn select_challenge<'a>(
&self,
challenges: &[&'a PaymentChallenge],
) -> Option<&'a PaymentChallenge> {
let first = challenges.first()?;
let provider = self
.providers
.iter()
.find(|provider| provider.dyn_supports(first.method.as_str(), first.intent.as_str()))?;
let candidates = challenges
.iter()
.copied()
.filter(|challenge| {
provider.dyn_supports(challenge.method.as_str(), challenge.intent.as_str())
})
.collect::<Vec<_>>();
provider.dyn_select_challenge(&candidates)
}
async fn pay(&self, challenge: &PaymentChallenge) -> Result<PaymentCredential, MppError> {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
return provider.dyn_pay(challenge).await;
}
}
Err(MppError::UnsupportedPaymentMethod(format!(
"no provider supports method={}, intent={}",
method, intent
)))
}
async fn pay_with_context(
&self,
challenge: &PaymentChallenge,
context: PaymentContext,
) -> Result<PaymentCredential, MppError> {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
return provider.dyn_pay_with_context(challenge, context).await;
}
}
Err(MppError::UnsupportedPaymentMethod(format!(
"no provider supports method={}, intent={}",
method, intent
)))
}
async fn prepare_http_payment_challenge(
&self,
challenge: &PaymentChallenge,
context: PaymentContext,
) -> Result<Option<PaymentChallenge>, MppError> {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
return provider
.dyn_prepare_http_payment_challenge(challenge, context)
.await;
}
}
Err(MppError::UnsupportedPaymentMethod(format!(
"no provider supports method={}, intent={}",
method, intent
)))
}
async fn prepare_application_websocket_challenge(
&self,
challenge: &PaymentChallenge,
context: PaymentContext,
) -> Result<PaymentChallenge, MppError> {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
return provider
.dyn_prepare_application_websocket_challenge(challenge, context)
.await;
}
}
Err(MppError::UnsupportedPaymentMethod(format!(
"no provider supports method={}, intent={}",
method, intent
)))
}
async fn commit_payment(
&self,
challenge: &PaymentChallenge,
credential: &PaymentCredential,
) -> Result<(), MppError> {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
return provider.dyn_commit_payment(challenge, credential).await;
}
}
Err(MppError::UnsupportedPaymentMethod(format!(
"no provider supports method={}, intent={}",
method, intent
)))
}
async fn rollback_payment(
&self,
challenge: &PaymentChallenge,
credential: &PaymentCredential,
) -> Result<(), MppError> {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
return provider.dyn_rollback_payment(challenge, credential).await;
}
}
Err(MppError::UnsupportedPaymentMethod(format!(
"no provider supports method={}, intent={}",
method, intent
)))
}
async fn invalidate_payment(
&self,
challenge: &PaymentChallenge,
credential: &PaymentCredential,
) -> Result<(), MppError> {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
return provider.dyn_invalidate_payment(challenge, credential).await;
}
}
Err(MppError::UnsupportedPaymentMethod(format!(
"no provider supports method={}, intent={}",
method, intent
)))
}
fn abandon_payment(&self, challenge: &PaymentChallenge, credential: &PaymentCredential) {
let method = challenge.method.as_str();
let intent = challenge.intent.as_str();
for provider in &self.providers {
if provider.dyn_supports(method, intent) {
provider.dyn_abandon_payment(challenge, credential);
return;
}
}
}
fn accept_payment_header(&self) -> Option<String> {
let mut entries: Vec<Entry> = Vec::new();
for provider in &self.providers {
let header = provider.dyn_accept_payment_header()?;
for entry in accept_payment::parse(&header).ok()? {
if entries
.iter()
.all(|seen| seen.method != entry.method || seen.intent != entry.intent)
{
entries.push(entry);
}
}
}
(!entries.is_empty()).then(|| accept_payment::serialize(&entries))
}
}
trait DynPaymentProvider: Send + Sync {
fn dyn_supports(&self, method: &str, intent: &str) -> bool;
fn dyn_select_challenge<'a>(
&self,
challenges: &[&'a PaymentChallenge],
) -> Option<&'a PaymentChallenge>;
fn dyn_pay<'a>(
&'a self,
challenge: &'a PaymentChallenge,
) -> std::pin::Pin<Box<dyn Future<Output = Result<PaymentCredential, MppError>> + Send + 'a>>;
fn dyn_pay_with_context<'a>(
&'a self,
challenge: &'a PaymentChallenge,
context: PaymentContext,
) -> std::pin::Pin<Box<dyn Future<Output = Result<PaymentCredential, MppError>> + Send + 'a>>;
fn dyn_prepare_http_payment_challenge<'a>(
&'a self,
challenge: &'a PaymentChallenge,
context: PaymentContext,
) -> std::pin::Pin<
Box<dyn Future<Output = Result<Option<PaymentChallenge>, MppError>> + Send + 'a>,
>;
fn dyn_prepare_application_websocket_challenge<'a>(
&'a self,
challenge: &'a PaymentChallenge,
context: PaymentContext,
) -> std::pin::Pin<Box<dyn Future<Output = Result<PaymentChallenge, MppError>> + Send + 'a>>;
fn dyn_commit_payment<'a>(
&'a self,
challenge: &'a PaymentChallenge,
credential: &'a PaymentCredential,
) -> std::pin::Pin<Box<dyn Future<Output = Result<(), MppError>> + Send + 'a>>;
fn dyn_rollback_payment<'a>(
&'a self,
challenge: &'a PaymentChallenge,
credential: &'a PaymentCredential,
) -> std::pin::Pin<Box<dyn Future<Output = Result<(), MppError>> + Send + 'a>>;
fn dyn_invalidate_payment<'a>(
&'a self,
challenge: &'a PaymentChallenge,
credential: &'a PaymentCredential,
) -> std::pin::Pin<Box<dyn Future<Output = Result<(), MppError>> + Send + 'a>>;
fn dyn_abandon_payment(&self, challenge: &PaymentChallenge, credential: &PaymentCredential);
fn dyn_accept_payment_header(&self) -> Option<String>;
fn clone_box(&self) -> Box<dyn DynPaymentProvider>;
}
impl<P: PaymentProvider + 'static> DynPaymentProvider for P {
fn dyn_supports(&self, method: &str, intent: &str) -> bool {
PaymentProvider::supports(self, method, intent)
}
fn dyn_select_challenge<'a>(
&self,
challenges: &[&'a PaymentChallenge],
) -> Option<&'a PaymentChallenge> {
PaymentProvider::select_challenge(self, challenges)
}
fn dyn_pay<'a>(
&'a self,
challenge: &'a PaymentChallenge,
) -> std::pin::Pin<Box<dyn Future<Output = Result<PaymentCredential, MppError>> + Send + 'a>>
{
Box::pin(PaymentProvider::pay(self, challenge))
}
fn dyn_pay_with_context<'a>(
&'a self,
challenge: &'a PaymentChallenge,
context: PaymentContext,
) -> std::pin::Pin<Box<dyn Future<Output = Result<PaymentCredential, MppError>> + Send + 'a>>
{
Box::pin(PaymentProvider::pay_with_context(self, challenge, context))
}
fn dyn_prepare_http_payment_challenge<'a>(
&'a self,
challenge: &'a PaymentChallenge,
context: PaymentContext,
) -> std::pin::Pin<
Box<dyn Future<Output = Result<Option<PaymentChallenge>, MppError>> + Send + 'a>,
> {
Box::pin(PaymentProvider::prepare_http_payment_challenge(
self, challenge, context,
))
}
fn dyn_prepare_application_websocket_challenge<'a>(
&'a self,
challenge: &'a PaymentChallenge,
context: PaymentContext,
) -> std::pin::Pin<Box<dyn Future<Output = Result<PaymentChallenge, MppError>> + Send + 'a>>
{
Box::pin(PaymentProvider::prepare_application_websocket_challenge(
self, challenge, context,
))
}
fn dyn_commit_payment<'a>(
&'a self,
challenge: &'a PaymentChallenge,
credential: &'a PaymentCredential,
) -> std::pin::Pin<Box<dyn Future<Output = Result<(), MppError>> + Send + 'a>> {
Box::pin(PaymentProvider::commit_payment(self, challenge, credential))
}
fn dyn_rollback_payment<'a>(
&'a self,
challenge: &'a PaymentChallenge,
credential: &'a PaymentCredential,
) -> std::pin::Pin<Box<dyn Future<Output = Result<(), MppError>> + Send + 'a>> {
Box::pin(PaymentProvider::rollback_payment(
self, challenge, credential,
))
}
fn dyn_invalidate_payment<'a>(
&'a self,
challenge: &'a PaymentChallenge,
credential: &'a PaymentCredential,
) -> std::pin::Pin<Box<dyn Future<Output = Result<(), MppError>> + Send + 'a>> {
Box::pin(PaymentProvider::invalidate_payment(
self, challenge, credential,
))
}
fn dyn_abandon_payment(&self, challenge: &PaymentChallenge, credential: &PaymentCredential) {
PaymentProvider::abandon_payment(self, challenge, credential);
}
fn dyn_accept_payment_header(&self) -> Option<String> {
PaymentProvider::accept_payment_header(self)
}
fn clone_box(&self) -> Box<dyn DynPaymentProvider> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn DynPaymentProvider> {
fn clone(&self) -> Self {
self.clone_box()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone)]
struct MockProvider {
method: &'static str,
intent: &'static str,
}
impl PaymentProvider for MockProvider {
fn supports(&self, method: &str, intent: &str) -> bool {
self.method == method && self.intent == intent
}
async fn pay(&self, challenge: &PaymentChallenge) -> Result<PaymentCredential, MppError> {
use crate::protocol::core::PaymentPayload;
Ok(PaymentCredential::new(
challenge.to_echo(),
PaymentPayload::hash(format!("mock-{}", self.method)),
))
}
}
#[derive(Clone)]
struct Advertising(Option<&'static str>);
impl PaymentProvider for Advertising {
fn supports(&self, _method: &str, _intent: &str) -> bool {
false
}
async fn pay(&self, challenge: &PaymentChallenge) -> Result<PaymentCredential, MppError> {
Err(MppError::UnsupportedPaymentMethod(
challenge.method.to_string(),
))
}
fn accept_payment_header(&self) -> Option<String> {
self.0.map(str::to_owned)
}
}
#[test]
fn multi_provider_accept_payment_merges_children() {
let multi = MultiProvider::new()
.with(Advertising(Some("tempo/session, tempo/charge;q=0.5")))
.with(Advertising(Some("tempo/charge, stripe/charge")));
assert_eq!(
multi.accept_payment_header().as_deref(),
Some("tempo/session, tempo/charge;q=0.5, stripe/charge")
);
}
#[test]
fn multi_provider_accept_payment_requires_every_child_to_advertise() {
assert_eq!(MultiProvider::new().accept_payment_header(), None);
for silent in [None, Some("not an accept-payment header")] {
let multi = MultiProvider::new()
.with(Advertising(Some("tempo/charge")))
.with(Advertising(silent));
assert_eq!(multi.accept_payment_header(), None);
}
}
#[cfg(all(feature = "tempo", feature = "stripe"))]
#[test]
fn multi_provider_advertises_built_in_providers() {
use crate::client::{StripeProvider, TempoProvider, TempoSessionProvider};
use crate::protocol::methods::stripe::CreateTokenResult;
use alloy::signers::local::PrivateKeySigner;
let rpc_url = "https://rpc.example.com";
let multi = MultiProvider::new()
.with(TempoSessionProvider::new(PrivateKeySigner::random(), rpc_url).unwrap())
.with(TempoProvider::new(PrivateKeySigner::random(), rpc_url).unwrap())
.with(StripeProvider::new(|_| {
Box::pin(async { Ok(CreateTokenResult::from("spt_test".to_string())) })
}));
assert_eq!(
multi.accept_payment_header().as_deref(),
Some("tempo/session, tempo/charge, stripe/charge")
);
}
#[test]
fn test_multi_provider_supports() {
let multi = MultiProvider::new()
.with(MockProvider {
method: "tempo",
intent: "charge",
})
.with(MockProvider {
method: "stripe",
intent: "charge",
});
assert!(multi.has_support("tempo", "charge"));
assert!(multi.has_support("stripe", "charge"));
assert!(!multi.has_support("bitcoin", "charge"));
assert!(!multi.has_support("tempo", "authorize"));
}
#[test]
fn test_multi_provider_empty() {
let multi = MultiProvider::new();
assert!(!multi.has_support("tempo", "charge"));
}
#[test]
fn test_multi_provider_clone() {
let multi = MultiProvider::new().with(MockProvider {
method: "tempo",
intent: "charge",
});
let cloned = multi.clone();
assert!(cloned.has_support("tempo", "charge"));
}
}