use std::str::FromStr;
use serde::{Deserialize, Serialize};
use super::version::Version2;
use crate::chain::ChainId;
use crate::error::VerificationError;
use crate::scheme::SchemeSlug;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TypedVerifyRequest<const V: u8, TPayload, TRequirements> {
pub x402_version: super::Version<V>,
pub payment_payload: TPayload,
pub payment_requirements: TRequirements,
}
impl<const V: u8, TPayload, TRequirements> TypedVerifyRequest<V, TPayload, TRequirements>
where
Self: serde::de::DeserializeOwned,
{
pub fn from_verify(request: VerifyRequest) -> Result<Self, VerificationError> {
serde_json::from_value(request.into_json())
.map_err(|e| VerificationError::InvalidFormat(e.to_string()))
}
pub fn from_settle(request: SettleRequest) -> Result<Self, VerificationError> {
serde_json::from_value(request.into_json())
.map_err(|e| VerificationError::InvalidFormat(e.to_string()))
}
}
impl<const V: u8, TPayload, TRequirements> TryFrom<TypedVerifyRequest<V, TPayload, TRequirements>>
for VerifyRequest
where
TPayload: Serialize,
TRequirements: Serialize,
{
type Error = serde_json::Error;
fn try_from(
value: TypedVerifyRequest<V, TPayload, TRequirements>,
) -> Result<Self, Self::Error> {
let json = serde_json::to_value(value)?;
Ok(Self(json))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifyRequest(serde_json::Value);
impl VerifyRequest {
#[must_use]
pub fn into_json(self) -> serde_json::Value {
self.0
}
#[must_use]
pub fn scheme_slug(&self) -> Option<SchemeSlug> {
scheme_slug_from_json(&self.0)
}
#[must_use]
pub fn network(&self) -> &str {
network_from_json(&self.0)
}
}
impl From<serde_json::Value> for VerifyRequest {
fn from(value: serde_json::Value) -> Self {
Self(value)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettleRequest(serde_json::Value);
impl SettleRequest {
#[must_use]
pub fn into_json(self) -> serde_json::Value {
self.0
}
#[must_use]
pub fn scheme_slug(&self) -> Option<SchemeSlug> {
scheme_slug_from_json(&self.0)
}
#[must_use]
pub fn network(&self) -> &str {
network_from_json(&self.0)
}
pub fn set_settlement_amount(&mut self, amount: &str) -> Result<(), VerificationError> {
let req = self
.0
.get_mut("paymentRequirements")
.and_then(serde_json::Value::as_object_mut)
.ok_or_else(|| {
VerificationError::InvalidFormat(
"settle request missing paymentRequirements object".into(),
)
})?;
let _ = req.insert(
"amount".to_owned(),
serde_json::Value::String(amount.to_owned()),
);
Ok(())
}
}
impl From<serde_json::Value> for SettleRequest {
fn from(value: serde_json::Value) -> Self {
Self(value)
}
}
impl From<VerifyRequest> for SettleRequest {
fn from(request: VerifyRequest) -> Self {
Self(request.into_json())
}
}
fn scheme_slug_from_json(json: &serde_json::Value) -> Option<SchemeSlug> {
let version = json.get("x402Version")?.as_u64()?;
let version: u8 = version.try_into().ok()?;
if version != Version2::VALUE {
return None;
}
let accepted = json.get("paymentPayload")?.get("accepted")?;
let chain_id = ChainId::from_str(accepted.get("network")?.as_str()?).ok()?;
let scheme = accepted.get("scheme")?.as_str()?;
Some(SchemeSlug::new(chain_id, scheme.into()))
}
fn network_from_json(json: &serde_json::Value) -> &str {
json.get("paymentRequirements")
.and_then(|r| r.get("network"))
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
fn v2_json(network: &str, scheme: &str) -> serde_json::Value {
serde_json::json!({
"x402Version": 2,
"paymentPayload": {
"accepted": { "network": network, "scheme": scheme }
},
"paymentRequirements": { "network": network }
})
}
#[test]
fn verify_request_scheme_slug_evm() {
let req = VerifyRequest::from(v2_json("eip155:8453", "exact"));
let slug = req.scheme_slug().unwrap();
assert_eq!(slug.to_string(), "eip155:8453:exact");
}
#[test]
fn settle_request_from_verify_preserves_slug() {
let verify = VerifyRequest::from(v2_json("eip155:42161", "exact"));
let settle: SettleRequest = verify.into();
assert_eq!(
settle.scheme_slug().unwrap().to_string(),
"eip155:42161:exact"
);
}
#[test]
fn settle_request_network_missing_returns_empty() {
let settle = SettleRequest::from(serde_json::json!({}));
assert_eq!(settle.network(), "");
}
#[test]
fn slug_rejects_wrong_version() {
let mut json = v2_json("eip155:1", "exact");
json["x402Version"] = serde_json::json!(99);
assert!(scheme_slug_from_json(&json).is_none());
}
#[test]
fn slug_rejects_invalid_caip2() {
assert!(scheme_slug_from_json(&v2_json("not-a-caip2", "exact")).is_none());
}
#[test]
fn settle_amount_override_rewrites_payment_requirements() {
let mut settle = SettleRequest::from(serde_json::json!({
"x402Version": 2,
"paymentPayload": { "accepted": { "network": "eip155:8453", "scheme": "upto" } },
"paymentRequirements": { "network": "eip155:8453", "amount": "5000000" }
}));
settle.set_settlement_amount("1500000").unwrap();
let json = settle.into_json();
assert_eq!(
json["paymentRequirements"]["amount"].as_str(),
Some("1500000")
);
}
#[test]
fn settle_amount_override_errors_when_requirements_missing() {
let mut settle = SettleRequest::from(serde_json::json!({}));
let err = settle.set_settlement_amount("1").unwrap_err();
assert!(matches!(err, VerificationError::InvalidFormat(_)));
}
}