mod extra;
use std::collections::HashMap;
use std::fmt::{self, Display, Formatter};
use std::str::FromStr;
pub use extra::{
apply_payment_flow_wire_extra, extra_payment_flow, is_authorization_payment_flow,
is_recognized_payment_flow,
};
use extra::{extra_string, requested_flow};
use r402_protocol::payment::PaymentRequirements;
use serde::{Deserialize, Serialize};
pub const SDK_DEFAULT_ASSET_TRANSFER_METHOD: &str = "default";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PaymentFlowName {
Authorization,
Upfront,
Escrow,
}
impl PaymentFlowName {
pub const ALL: [Self; 3] = [Self::Authorization, Self::Upfront, Self::Escrow];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Authorization => "authorization",
Self::Upfront => "upfront",
Self::Escrow => "escrow",
}
}
#[must_use]
pub const fn phases(self) -> PaymentFlowPhases {
match self {
Self::Authorization => PaymentFlowPhases {
verify_before_handler: true,
settle_before_handler: false,
settle_after_handler: true,
},
Self::Upfront => PaymentFlowPhases {
verify_before_handler: false,
settle_before_handler: true,
settle_after_handler: false,
},
Self::Escrow => PaymentFlowPhases {
verify_before_handler: false,
settle_before_handler: true,
settle_after_handler: true,
},
}
}
}
impl Display for PaymentFlowName {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for PaymentFlowName {
type Err = PaymentFlowError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"authorization" => Ok(Self::Authorization),
"upfront" => Ok(Self::Upfront),
"escrow" => Ok(Self::Escrow),
other => Err(PaymentFlowError::UnknownPaymentFlow {
flow: other.to_owned(),
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum SettlePhase {
BeforeHandler,
AfterHandler,
Cancel,
}
impl SettlePhase {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::BeforeHandler => "before-handler",
Self::AfterHandler => "after-handler",
Self::Cancel => "cancel",
}
}
}
impl Display for SettlePhase {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for SettlePhase {
type Err = PaymentFlowError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"before-handler" => Ok(Self::BeforeHandler),
"after-handler" => Ok(Self::AfterHandler),
"cancel" => Ok(Self::Cancel),
other => Err(PaymentFlowError::UnknownSettlePhase {
phase: other.to_owned(),
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(
clippy::struct_excessive_bools,
reason = "PAYMENT_FLOWS phase flags are a closed triple"
)]
pub struct PaymentFlowPhases {
pub verify_before_handler: bool,
pub settle_before_handler: bool,
pub settle_after_handler: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PaymentFlowConfig {
pub supported: Vec<PaymentFlowName>,
pub default: PaymentFlowName,
}
impl PaymentFlowConfig {
#[must_use]
pub const fn new(supported: Vec<PaymentFlowName>, default: PaymentFlowName) -> Self {
Self { supported, default }
}
#[must_use]
pub fn authorization_and_upfront() -> Self {
Self::new(
vec![PaymentFlowName::Authorization, PaymentFlowName::Upfront],
PaymentFlowName::Authorization,
)
}
#[must_use]
pub fn authorization_only() -> Self {
Self::new(
vec![PaymentFlowName::Authorization],
PaymentFlowName::Authorization,
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct PaymentFlowScheme<'a> {
pub scheme: &'a str,
pub default_asset_transfer_method: &'a str,
pub payment_flows: &'a HashMap<String, PaymentFlowConfig>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedPaymentFlow {
pub asset_transfer_method: String,
pub payment_flow: PaymentFlowName,
}
pub const PAYMENT_FLOWS: [(PaymentFlowName, PaymentFlowPhases); 3] = [
(
PaymentFlowName::Authorization,
PaymentFlowName::Authorization.phases(),
),
(PaymentFlowName::Upfront, PaymentFlowName::Upfront.phases()),
(PaymentFlowName::Escrow, PaymentFlowName::Escrow.phases()),
];
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum PaymentFlowError {
#[error(
"[x402] Scheme \"{scheme}\" does not support assetTransferMethod \"{asset_transfer_method}\". Supported: {supported}."
)]
UnsupportedAssetTransferMethod {
scheme: String,
asset_transfer_method: String,
supported: String,
},
#[error(
"[x402] Scheme \"{scheme}\" paymentFlows[\"{asset_transfer_method}\"].default is not in supported."
)]
DefaultNotInSupported {
scheme: String,
asset_transfer_method: String,
},
#[error(
"[x402] Scheme \"{scheme}\" assetTransferMethod \"{asset_transfer_method}\" does not support paymentFlow \"{requested}\". Supported: {supported} (default: {default})."
)]
UnsupportedPaymentFlow {
scheme: String,
asset_transfer_method: String,
requested: String,
supported: String,
default: String,
},
#[error(
"[x402] Unknown payment flow \"{flow}\". Expected one of: authorization, upfront, escrow."
)]
UnknownPaymentFlow {
flow: String,
},
#[error(
"[x402] Unknown settle phase \"{phase}\". Expected one of: before-handler, after-handler, cancel."
)]
UnknownSettlePhase {
phase: String,
},
#[error("[x402] No server implementation registered for scheme: {scheme}, network: {network}")]
UnregisteredScheme {
scheme: String,
network: String,
},
}
#[must_use]
pub const fn resolve_payment_flow_phases(flow: PaymentFlowName) -> PaymentFlowPhases {
flow.phases()
}
pub fn resolve_payment_flow(
scheme: &PaymentFlowScheme<'_>,
requirements: &PaymentRequirements,
) -> Result<ResolvedPaymentFlow, PaymentFlowError> {
let atm = extra_string(requirements, "assetTransferMethod")
.unwrap_or(scheme.default_asset_transfer_method);
let Some(config) = scheme.payment_flows.get(atm) else {
return Err(PaymentFlowError::UnsupportedAssetTransferMethod {
scheme: scheme.scheme.to_owned(),
asset_transfer_method: atm.to_owned(),
supported: join_sorted_keys(scheme.payment_flows),
});
};
if !config.supported.contains(&config.default) {
return Err(PaymentFlowError::DefaultNotInSupported {
scheme: scheme.scheme.to_owned(),
asset_transfer_method: atm.to_owned(),
});
}
let flow = match requested_flow(requirements) {
None => config.default,
Some(label) => match PaymentFlowName::from_str(&label) {
Ok(name) if config.supported.contains(&name) => name,
Ok(name) => {
return Err(unsupported_flow(scheme, atm, name.as_str(), config));
}
Err(_) => return Err(unsupported_flow(scheme, atm, &label, config)),
},
};
Ok(ResolvedPaymentFlow {
asset_transfer_method: atm.to_owned(),
payment_flow: flow,
})
}
fn unsupported_flow(
scheme: &PaymentFlowScheme<'_>,
atm: &str,
requested: &str,
config: &PaymentFlowConfig,
) -> PaymentFlowError {
PaymentFlowError::UnsupportedPaymentFlow {
scheme: scheme.scheme.to_owned(),
asset_transfer_method: atm.to_owned(),
requested: requested.to_owned(),
supported: join_names(&config.supported),
default: config.default.as_str().to_owned(),
}
}
fn join_names(names: &[PaymentFlowName]) -> String {
names
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>()
.join(", ")
}
fn join_sorted_keys(flows: &HashMap<String, PaymentFlowConfig>) -> String {
let mut keys: Vec<&str> = flows.keys().map(String::as_str).collect();
keys.sort_unstable();
keys.join(", ")
}