use crate::error::MppError;
use crate::protocol::core::{PaymentChallenge, PaymentCredential};
use std::future::Future;
pub trait PaymentProvider: Clone + Send + Sync {
fn supports(&self, method: &str, intent: &str) -> bool;
fn pay(
&self,
challenge: &PaymentChallenge,
) -> impl Future<Output = Result<PaymentCredential, MppError>> + Send;
}
#[cfg(feature = "tempo")]
#[derive(Clone)]
pub struct TempoProvider {
signer: alloy_signer_local::PrivateKeySigner,
rpc_url: reqwest::Url,
}
#[cfg(feature = "tempo")]
impl TempoProvider {
pub fn new(
signer: alloy_signer_local::PrivateKeySigner,
rpc_url: impl AsRef<str>,
) -> Result<Self, MppError> {
let url = rpc_url
.as_ref()
.parse()
.map_err(|e| MppError::InvalidConfig(format!("invalid RPC URL: {}", e)))?;
Ok(Self {
signer,
rpc_url: url,
})
}
pub fn signer(&self) -> &alloy_signer_local::PrivateKeySigner {
&self.signer
}
pub fn rpc_url(&self) -> &reqwest::Url {
&self.rpc_url
}
}
#[cfg(feature = "tempo")]
impl PaymentProvider for TempoProvider {
fn supports(&self, method: &str, intent: &str) -> bool {
method == "tempo" && intent == "charge"
}
async fn pay(&self, challenge: &PaymentChallenge) -> Result<PaymentCredential, MppError> {
use crate::protocol::core::PaymentPayload;
use crate::protocol::intents::ChargeRequest;
use crate::protocol::methods::tempo::{TempoChargeExt, CHAIN_ID};
use alloy::network::{EthereumWallet, TransactionBuilder};
use alloy::providers::{Provider, ProviderBuilder};
use tempo_alloy::contracts::precompiles::tip20::ITIP20;
use tempo_alloy::TempoNetwork;
let charge: ChargeRequest = challenge.request.decode()?;
let expected_chain_id = charge.chain_id().unwrap_or(CHAIN_ID);
let address = self.signer.address();
let wallet = EthereumWallet::from(self.signer.clone());
let provider = ProviderBuilder::new_with_network::<TempoNetwork>()
.wallet(wallet.clone())
.connect_http(self.rpc_url.clone());
let actual_chain_id: u64 = provider
.get_chain_id()
.await
.map_err(|e| MppError::Http(format!("failed to get chain ID: {}", e)))?;
if actual_chain_id != expected_chain_id {
return Err(MppError::ChainIdMismatch {
expected: expected_chain_id,
got: actual_chain_id,
});
}
let recipient = charge.recipient_address()?;
let amount = charge.amount_u256()?;
let currency = charge.currency_address()?;
let token = ITIP20::new(currency, &provider);
let call = token.transfer(recipient, amount);
let tx = call
.into_transaction_request()
.with_from(address)
.with_chain_id(expected_chain_id);
let tx_envelope = tx
.build(&wallet)
.await
.map_err(|e| MppError::Http(format!("failed to sign transaction: {}", e)))?;
let signed_tx_bytes = alloy::rlp::encode(&tx_envelope);
let signed_tx_hex = format!("0x{}", hex::encode(&signed_tx_bytes));
let echo = challenge.to_echo();
Ok(PaymentCredential::with_source(
echo,
format!("did:pkh:eip155:{}:{}", expected_chain_id, address),
PaymentPayload::transaction(signed_tx_hex),
))
}
}
#[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)
}
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
)))
}
}
trait DynPaymentProvider: Send + Sync {
fn dyn_supports(&self, method: &str, intent: &str) -> bool;
fn dyn_pay<'a>(
&'a self,
challenge: &'a PaymentChallenge,
) -> std::pin::Pin<Box<dyn Future<Output = Result<PaymentCredential, MppError>> + Send + 'a>>;
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_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 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)),
))
}
}
#[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"));
}
#[cfg(feature = "tempo")]
#[test]
fn test_tempo_provider_new() {
let signer = alloy_signer_local::PrivateKeySigner::random();
let provider = TempoProvider::new(signer.clone(), "https://rpc.example.com").unwrap();
assert_eq!(provider.rpc_url().as_str(), "https://rpc.example.com/");
assert_eq!(provider.signer().address(), signer.address());
}
#[cfg(feature = "tempo")]
#[test]
fn test_tempo_provider_invalid_url() {
let signer = alloy_signer_local::PrivateKeySigner::random();
let result = TempoProvider::new(signer, "not a url");
assert!(result.is_err());
}
}