use std::sync::Arc;
use crate::error::{MppError, ResultExt};
use crate::protocol::core::{PaymentChallenge, PaymentCredential};
use super::autoswap::AutoswapConfig;
use super::charge::SignOptions;
use super::signing::TempoSigningMode;
use crate::client::PaymentProvider;
#[derive(Clone)]
pub struct TempoProvider {
signer: Arc<dyn alloy::signers::Signer + Send + Sync>,
rpc_url: reqwest::Url,
client_id: Option<String>,
signing_mode: TempoSigningMode,
autoswap: Option<AutoswapConfig>,
}
impl TempoProvider {
pub fn new(
signer: impl alloy::signers::Signer + Send + Sync + 'static,
rpc_url: impl AsRef<str>,
) -> Result<Self, MppError> {
let url = rpc_url.as_ref().parse().mpp_config("invalid RPC URL")?;
Ok(Self {
signer: Arc::new(signer),
rpc_url: url,
client_id: None,
signing_mode: TempoSigningMode::Direct,
autoswap: None,
})
}
pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
self.client_id = Some(client_id.into());
self
}
pub fn with_signing_mode(mut self, mode: TempoSigningMode) -> Self {
self.signing_mode = mode;
self
}
pub fn with_autoswap(mut self, config: AutoswapConfig) -> Self {
self.autoswap = Some(config);
self
}
pub fn autoswap(&self) -> Option<&AutoswapConfig> {
self.autoswap.as_ref()
}
pub fn signing_mode(&self) -> &TempoSigningMode {
&self.signing_mode
}
pub fn signer(&self) -> &(dyn alloy::signers::Signer + Send + Sync) {
&*self.signer
}
pub fn rpc_url(&self) -> &reqwest::Url {
&self.rpc_url
}
}
impl PaymentProvider for TempoProvider {
fn supports(&self, method: &str, intent: &str) -> bool {
method == crate::protocol::methods::tempo::METHOD_NAME
&& intent == crate::protocol::methods::tempo::INTENT_CHARGE
}
async fn pay(&self, challenge: &PaymentChallenge) -> Result<PaymentCredential, MppError> {
let mut charge = super::charge::TempoCharge::from_challenge(challenge)?;
if charge.memo().is_none() {
let memo =
crate::tempo::attribution::encode(&challenge.realm, self.client_id.as_deref());
charge = charge.with_memo(memo);
}
if let Some(autoswap_config) = &self.autoswap {
let from = self.signing_mode.from_address(self.signer.address());
let rpc_url: reqwest::Url = self.rpc_url.clone();
let provider =
alloy::providers::RootProvider::<tempo_alloy::TempoNetwork>::new_http(rpc_url);
if let Some(swap_call) = super::autoswap::resolve_autoswap(
&provider,
from,
charge.currency(),
charge.amount(),
autoswap_config,
)
.await?
{
charge = charge.with_prepended_call(swap_call);
}
}
let options = SignOptions {
rpc_url: Some(self.rpc_url.to_string()),
signing_mode: Some(self.signing_mode.clone()),
..Default::default()
};
let signed = charge
.sign_with_options(self.signer.as_ref(), options)
.await?;
Ok(signed.into_credential())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tempo_provider_new() {
let signer = alloy::signers::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());
}
#[test]
fn test_tempo_provider_invalid_url() {
let signer = alloy::signers::local::PrivateKeySigner::random();
let result = TempoProvider::new(signer, "not a url");
assert!(result.is_err());
}
#[test]
fn test_tempo_provider_with_client_id() {
let signer = alloy::signers::local::PrivateKeySigner::random();
let provider = TempoProvider::new(signer, "https://rpc.example.com")
.unwrap()
.with_client_id("my-app");
assert_eq!(provider.client_id.as_deref(), Some("my-app"));
}
#[test]
fn test_tempo_provider_default_signing_mode() {
let signer = alloy::signers::local::PrivateKeySigner::random();
let provider = TempoProvider::new(signer, "https://rpc.example.com").unwrap();
assert!(matches!(provider.signing_mode(), TempoSigningMode::Direct));
}
#[test]
fn test_tempo_provider_with_signing_mode() {
use crate::client::tempo::signing::KeychainVersion;
let signer = alloy::signers::local::PrivateKeySigner::random();
let wallet: alloy::primitives::Address = "0x1111111111111111111111111111111111111111"
.parse()
.unwrap();
let provider = TempoProvider::new(signer, "https://rpc.example.com")
.unwrap()
.with_signing_mode(TempoSigningMode::Keychain {
wallet,
key_authorization: None,
version: KeychainVersion::V1,
});
assert!(matches!(
provider.signing_mode(),
TempoSigningMode::Keychain { .. }
));
}
#[test]
fn test_tempo_provider_supports() {
let signer = alloy::signers::local::PrivateKeySigner::random();
let provider = TempoProvider::new(signer, "https://rpc.example.com").unwrap();
assert!(provider.supports("tempo", "charge"));
assert!(!provider.supports("tempo", "session"));
assert!(!provider.supports("stripe", "charge"));
}
#[test]
fn test_auto_generated_memo_is_mpp_memo() {
let memo = crate::tempo::attribution::encode("api.example.com", Some("my-app"));
assert!(crate::tempo::attribution::is_mpp_memo(&memo));
}
#[test]
fn test_tempo_provider_supports_only_tempo_charge() {
let signer = alloy::signers::local::PrivateKeySigner::random();
let provider = TempoProvider::new(signer, "https://rpc.example.com").unwrap();
assert!(provider.supports("tempo", "charge"));
assert!(!provider.supports("tempo", "session"));
assert!(!provider.supports("tempo", "open"));
assert!(!provider.supports("stripe", "charge"));
assert!(!provider.supports("", ""));
assert!(!provider.supports("TEMPO", "charge"));
}
#[test]
fn test_user_memo_takes_precedence() {
let user_memo = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
let hex_str = user_memo.strip_prefix("0x").unwrap();
let bytes = hex::decode(hex_str).unwrap();
let memo_bytes: [u8; 32] = bytes.try_into().unwrap();
assert!(!crate::tempo::attribution::is_mpp_memo(&memo_bytes));
}
}