use std::sync::Arc;
#[cfg(any(feature = "tempo", feature = "stripe"))]
use crate::error::Result;
use crate::protocol::core::Base64UrlJson;
use crate::protocol::traits::ChargeMethod;
use crate::server::events::{
PaymentSuccessContext, ServerEvent, ServerEventKind, ServerEventSubscription, ServerEvents,
};
#[cfg(any(feature = "tempo", feature = "stripe"))]
mod issue;
mod session;
mod verify;
pub use session::SessionVerifyResult;
const SECRET_KEY_ENV_VAR: &str = "MPP_SECRET_KEY";
const DEFAULT_DECIMALS: u32 = 6;
const REALM_ENV_VARS: &[&str] = &[
"MPP_REALM",
"FLY_APP_NAME",
"HEROKU_APP_NAME",
"RAILWAY_PUBLIC_DOMAIN",
"RENDER_EXTERNAL_HOSTNAME",
"VERCEL_URL",
"WEBSITE_HOSTNAME",
];
const DEFAULT_REALM: &str = "MPP Payment";
#[cfg(any(feature = "tempo", feature = "stripe"))]
fn advertised_builder_credential_header(requires_auth: bool) -> Option<String> {
requires_auth.then(|| crate::protocol::core::PAYMENT_AUTHORIZATION_HEADER.to_string())
}
pub(crate) fn detect_realm() -> String {
realm_from_env(|name| std::env::var(name).ok())
}
fn realm_from_env(lookup: impl Fn(&str) -> Option<String>) -> String {
REALM_ENV_VARS
.iter()
.find_map(|name| lookup(name).filter(|value| !value.is_empty()))
.unwrap_or_else(|| DEFAULT_REALM.to_string())
}
#[derive(Clone)]
pub struct Mpp<M, S = ()> {
method: Arc<M>,
session_method: Option<Arc<S>>,
config: Arc<Config>,
events: ServerEvents,
}
#[derive(Clone)]
struct Config {
realm: String,
secret_key: String,
currencies: Vec<String>,
recipient: Option<String>,
decimals: u32,
fee_payer: bool,
machine_token_enabled: bool,
chain_id: Option<u64>,
opaque: Option<Base64UrlJson>,
credential_header: Option<String>,
}
impl Config {
fn new(realm: String, secret_key: String) -> Self {
Self {
realm,
secret_key,
currencies: Vec::new(),
recipient: None,
decimals: DEFAULT_DECIMALS,
fee_payer: false,
machine_token_enabled: false,
chain_id: None,
opaque: None,
credential_header: None,
}
}
}
impl<M> Mpp<M, ()>
where
M: ChargeMethod,
{
pub fn new(method: M, realm: impl Into<String>, secret_key: impl Into<String>) -> Mpp<M, ()> {
Self::from_config(method, Config::new(realm.into(), secret_key.into()))
}
pub fn new_with_config(
method: M,
realm: impl Into<String>,
secret_key: impl Into<String>,
currency: impl Into<String>,
recipient: impl Into<String>,
) -> Self {
Self::from_config(
method,
Config {
currencies: vec![currency.into()],
recipient: Some(recipient.into()),
..Config::new(realm.into(), secret_key.into())
},
)
}
fn from_config(method: M, config: Config) -> Self {
Mpp {
method: Arc::new(method),
session_method: None,
config: Arc::new(config),
events: ServerEvents::default(),
}
}
}
impl<M, S> Mpp<M, S>
where
M: ChargeMethod,
{
pub fn with_session_method<S2>(self, session_method: S2) -> Mpp<M, S2> {
Mpp {
method: self.method,
session_method: Some(Arc::new(session_method)),
config: self.config,
events: self.events,
}
}
pub fn with_events(mut self, events: ServerEvents) -> Self {
self.events = events;
self
}
pub fn with_opaque(mut self, opaque: Base64UrlJson) -> Self {
Arc::make_mut(&mut self.config).opaque = Some(opaque);
self
}
pub fn with_requires_auth(mut self, enabled: bool) -> Self {
Arc::make_mut(&mut self.config).credential_header =
enabled.then(|| crate::protocol::core::PAYMENT_AUTHORIZATION_HEADER.to_string());
self
}
pub fn requires_auth(&self) -> bool {
self.config.credential_header.is_some()
}
pub fn credential_header(&self) -> &str {
self.config
.credential_header
.as_deref()
.unwrap_or("Authorization")
}
pub fn events(&self) -> ServerEvents {
self.events.clone()
}
pub fn on<F, Fut>(&self, kind: ServerEventKind, handler: F) -> ServerEventSubscription
where
F: Fn(ServerEvent) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
self.events.on(kind, handler)
}
pub fn on_any<F, Fut>(&self, handler: F) -> ServerEventSubscription
where
F: Fn(ServerEvent) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
self.events.on_any(handler)
}
pub fn on_payment_success<F, Fut>(&self, handler: F) -> ServerEventSubscription
where
F: Fn(PaymentSuccessContext) -> Fut + Send + Sync + 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
self.events.on_payment_success(handler)
}
pub fn realm(&self) -> &str {
&self.config.realm
}
pub fn method_name(&self) -> &str {
self.method.method()
}
pub fn currency(&self) -> Option<&str> {
self.config.currencies.first().map(String::as_str)
}
pub fn currencies(&self) -> &[String] {
&self.config.currencies
}
pub fn recipient(&self) -> Option<&str> {
self.config.recipient.as_deref()
}
pub fn decimals(&self) -> u32 {
self.config.decimals
}
pub fn fee_payer(&self) -> bool {
self.config.fee_payer
}
pub fn machine_token_enabled(&self) -> bool {
self.config.machine_token_enabled
}
pub fn chain_id(&self) -> Option<u64> {
self.config.chain_id
}
#[cfg(test)]
pub(crate) fn with_currencies(mut self, currencies: Vec<String>) -> Self {
Arc::make_mut(&mut self.config).currencies = currencies;
self
}
}
#[cfg(feature = "tempo")]
impl Mpp<super::TempoChargeMethod<super::TempoProvider>> {
pub fn create(mut builder: super::TempoBuilder) -> Result<Self> {
builder
.chain_id
.get_or_insert_with(|| super::tempo::chain_id_from_rpc_url(&builder.rpc_url));
if builder.fee_payer_fee_token.is_some() && builder.fee_payer_signer.is_none() {
return Err(crate::error::MppError::InvalidConfig(
"fee_payer_fee_token requires a local fee payer signer".into(),
));
}
if builder.fee_payer && builder.fee_payer_signer.is_none() && builder.relay.is_none() {
return Err(crate::error::MppError::InvalidConfig(
"fee_payer(true) requires fee_payer_signer(...) or relay(...)".into(),
));
}
if builder
.fee_payer_allowed_fee_tokens
.as_ref()
.is_some_and(Vec::is_empty)
{
return Err(crate::error::MppError::InvalidConfig(
"fee_payer_allowed_fee_tokens must contain at least one token".into(),
));
}
if builder.machine_token_enabled {
let chain_id = builder
.chain_id
.unwrap_or(crate::protocol::methods::tempo::CHAIN_ID);
if !crate::protocol::methods::tempo::machine_token::is_supported(chain_id) {
return Err(crate::error::MppError::InvalidConfig(format!(
"machine tokens are not supported on chain ID {chain_id}"
)));
}
}
let currencies = super::tempo::resolve_currencies(&builder)?;
let secret_key = builder
.secret_key
.or_else(|| std::env::var(SECRET_KEY_ENV_VAR).ok())
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| {
crate::error::MppError::InvalidConfig(format!(
"Missing secret key. Set {} environment variable or pass .secret_key(...).",
SECRET_KEY_ENV_VAR
))
})?;
crate::protocol::core::validate_secret_key(&secret_key)?;
let provider = super::tempo_provider(&builder.rpc_url)?;
let mut method = crate::protocol::methods::tempo::ChargeMethod::new(provider);
if let Some(signer) = builder.fee_payer_signer {
method = method.with_fee_payer_arc(signer);
}
if let Some(allowed_fee_tokens) = builder.fee_payer_allowed_fee_tokens {
method = method.with_fee_payer_allowed_fee_tokens(allowed_fee_tokens);
}
if let Some(fee_token) = builder.fee_payer_fee_token {
method = method.with_fee_payer_fee_token(fee_token);
}
if let Some(store) = builder.store {
method = method.with_store(store);
}
if let Some(relay) = builder.relay {
method = method.with_relay(relay)?;
}
Ok(Self::from_config(
method,
Config {
currencies,
recipient: Some(builder.recipient),
decimals: builder.decimals,
fee_payer: builder.fee_payer,
machine_token_enabled: builder.machine_token_enabled,
chain_id: builder.chain_id,
credential_header: advertised_builder_credential_header(builder.requires_auth),
..Config::new(builder.realm, secret_key)
},
))
}
}
#[cfg(feature = "stripe")]
impl Mpp<crate::protocol::methods::stripe::method::ChargeMethod> {
pub fn create_stripe(builder: super::StripeBuilder) -> Result<Self> {
let secret_key = builder
.hmac_secret_key
.or_else(|| std::env::var(SECRET_KEY_ENV_VAR).ok())
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| {
crate::error::MppError::InvalidConfig(format!(
"Missing secret key. Set {} environment variable or pass .secret_key(...).",
SECRET_KEY_ENV_VAR
))
})?;
crate::protocol::core::validate_secret_key(&secret_key)?;
let mut method = crate::protocol::methods::stripe::method::ChargeMethod::new(
&builder.secret_key,
&builder.network_id,
builder.payment_method_types.clone(),
);
if let Some(api_base) = builder.stripe_api_base {
method = method.with_api_base(api_base);
}
Ok(Self::from_config(
method,
Config {
currencies: vec![builder.currency],
decimals: builder.decimals as u32,
credential_header: advertised_builder_credential_header(builder.requires_auth),
..Config::new(builder.realm, secret_key)
},
))
}
}
#[cfg(test)]
#[allow(deprecated)]
mod tests;