use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use crate::chain::ChainId;
use crate::proto;
use crate::proto::SupportedResponse;
pub type Version2 = super::Version<2>;
pub const V2: Version2 = super::Version;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceInfo {
pub description: String,
pub mime_type: String,
pub url: String,
}
pub type VerifyRequest<TPayload, TRequirements> =
proto::TypedVerifyRequest<2, TPayload, TRequirements>;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentPayload<TAccepted, TPayload> {
pub accepted: TAccepted,
pub payload: TPayload,
pub resource: Option<ResourceInfo>,
pub x402_version: Version2,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extensions: Option<proto::Extensions>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PaymentRequirements<
TScheme = String,
TAmount = String,
TAddress = String,
TExtra = serde_json::Value,
> {
pub scheme: TScheme,
pub network: ChainId,
pub amount: TAmount,
pub pay_to: TAddress,
pub max_timeout_seconds: u64,
pub asset: TAddress,
#[serde(skip_serializing_if = "Option::is_none")]
pub extra: Option<TExtra>,
}
impl PaymentRequirements {
#[must_use]
pub fn as_concrete<
TScheme: FromStr,
TAmount: FromStr,
TAddress: FromStr,
TExtra: DeserializeOwned,
>(
&self,
) -> Option<PaymentRequirements<TScheme, TAmount, TAddress, TExtra>> {
let scheme = self.scheme.parse::<TScheme>().ok()?;
let amount = self.amount.parse::<TAmount>().ok()?;
let pay_to = self.pay_to.parse::<TAddress>().ok()?;
let asset = self.asset.parse::<TAddress>().ok()?;
let extra = self
.extra
.as_ref()
.and_then(|v| serde_json::from_value(v.clone()).ok());
Some(PaymentRequirements {
scheme,
network: self.network.clone(),
amount,
pay_to,
max_timeout_seconds: self.max_timeout_seconds,
asset,
extra,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PaymentRequired {
pub x402_version: Version2,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub resource: ResourceInfo,
#[serde(default)]
pub accepts: Vec<PaymentRequirements>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub extensions: Option<proto::Extensions>,
}
#[derive(Clone)]
pub struct PriceTag {
pub requirements: PaymentRequirements,
#[doc(hidden)]
pub enricher: Option<Enricher>,
}
impl fmt::Debug for PriceTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PriceTag")
.field("requirements", &self.requirements)
.field("enricher", &self.enricher.as_ref().map(|_| "<fn>"))
.finish()
}
}
pub type Enricher = Arc<dyn Fn(&mut PriceTag, &SupportedResponse) + Send + Sync>;
impl PriceTag {
pub fn enrich(&mut self, capabilities: &SupportedResponse) {
if let Some(enricher) = self.enricher.clone() {
enricher(self, capabilities);
}
}
#[must_use]
pub const fn with_timeout(mut self, seconds: u64) -> Self {
self.requirements.max_timeout_seconds = seconds;
self
}
}
impl PartialEq<PaymentRequirements> for PriceTag {
fn eq(&self, b: &PaymentRequirements) -> bool {
let a = &self.requirements;
a.scheme == b.scheme
&& a.network == b.network
&& a.amount == b.amount
&& a.asset == b.asset
&& a.pay_to == b.pay_to
}
}