use std::str::FromStr;
use serde::{Deserialize, Serialize};
use super::{PaymentVerificationError, v2};
use crate::chain::ChainId;
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_proto(request: VerifyRequest) -> Result<Self, PaymentVerificationError> {
let deserialized: Self = serde_json::from_value(request.into_json())?;
Ok(deserialized)
}
pub fn from_settle(request: SettleRequest) -> Result<Self, PaymentVerificationError> {
let deserialized: Self = serde_json::from_value(request.into_json())?;
Ok(deserialized)
}
}
impl<const V: u8, TPayload, TRequirements> TryInto<VerifyRequest>
for TypedVerifyRequest<V, TPayload, TRequirements>
where
TPayload: Serialize,
TRequirements: Serialize,
{
type Error = serde_json::Error;
fn try_into(self) -> Result<VerifyRequest, Self::Error> {
let json = serde_json::to_value(self)?;
Ok(VerifyRequest(json))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifyRequest(serde_json::Value);
impl From<serde_json::Value> for VerifyRequest {
fn from(value: serde_json::Value) -> Self {
Self(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)
}
}
#[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 {
self.0
.get("paymentRequirements")
.and_then(|r| r.get("network"))
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
}
}
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 x402_version: u8 = json.get("x402Version")?.as_u64()?.try_into().ok()?;
if x402_version != v2::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()))
}
#[cfg(test)]
#[allow(
clippy::indexing_slicing,
reason = "test mutations on known JSON structure"
)]
mod tests {
use super::*;
fn make_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 slug_extracts_evm_exact() {
let json = make_v2_json("eip155:8453", "exact");
let slug = scheme_slug_from_json(&json).unwrap();
assert_eq!(slug.chain_id, ChainId::new("eip155", "8453"));
assert_eq!(slug.name, "exact");
}
#[test]
fn slug_extracts_solana_exact() {
let json = make_v2_json("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "exact");
let slug = scheme_slug_from_json(&json).unwrap();
assert_eq!(slug.chain_id.namespace(), "solana");
assert_eq!(slug.name, "exact");
}
#[test]
fn slug_rejects_wrong_version() {
let mut json = make_v2_json("eip155:1", "exact");
json["x402Version"] = serde_json::json!(99);
assert!(scheme_slug_from_json(&json).is_none());
}
#[test]
fn slug_rejects_missing_payload() {
let json = serde_json::json!({"x402Version": 2});
assert!(scheme_slug_from_json(&json).is_none());
}
#[test]
fn slug_rejects_missing_network() {
let json = serde_json::json!({
"x402Version": 2,
"paymentPayload": {"accepted": {"scheme": "exact"}}
});
assert!(scheme_slug_from_json(&json).is_none());
}
#[test]
fn slug_rejects_invalid_caip2() {
let json = make_v2_json("not-a-caip2", "exact");
assert!(scheme_slug_from_json(&json).is_none());
}
#[test]
fn verify_request_scheme_slug() {
let json = make_v2_json("eip155:1", "exact");
let req = VerifyRequest::from(json);
let slug = req.scheme_slug().unwrap();
assert_eq!(slug.to_string(), "eip155:1:exact");
}
#[test]
fn settle_request_from_verify_preserves_slug() {
let json = make_v2_json("eip155:42161", "exact");
let verify = VerifyRequest::from(json);
let settle: SettleRequest = verify.into();
let slug = settle.scheme_slug().unwrap();
assert_eq!(slug.to_string(), "eip155:42161:exact");
}
#[test]
fn settle_request_network() {
let json = make_v2_json("eip155:8453", "exact");
let settle = SettleRequest::from(json);
assert_eq!(settle.network(), "eip155:8453");
}
#[test]
fn settle_request_network_missing_returns_empty() {
let settle = SettleRequest::from(serde_json::json!({}));
assert_eq!(settle.network(), "");
}
}