r402_core/wire/
request.rs1use std::str::FromStr;
4
5use serde::{Deserialize, Serialize};
6
7use super::version::Version2;
8use crate::chain::ChainId;
9use crate::error::VerificationError;
10use crate::scheme::SchemeSlug;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct TypedVerifyRequest<const V: u8, TPayload, TRequirements> {
21 pub x402_version: super::Version<V>,
23 pub payment_payload: TPayload,
25 pub payment_requirements: TRequirements,
27}
28
29impl<const V: u8, TPayload, TRequirements> TypedVerifyRequest<V, TPayload, TRequirements>
30where
31 Self: serde::de::DeserializeOwned,
32{
33 pub fn from_verify(request: VerifyRequest) -> Result<Self, VerificationError> {
39 serde_json::from_value(request.into_json())
40 .map_err(|e| VerificationError::InvalidFormat(e.to_string()))
41 }
42
43 pub fn from_settle(request: SettleRequest) -> Result<Self, VerificationError> {
49 serde_json::from_value(request.into_json())
50 .map_err(|e| VerificationError::InvalidFormat(e.to_string()))
51 }
52}
53
54impl<const V: u8, TPayload, TRequirements> TryFrom<TypedVerifyRequest<V, TPayload, TRequirements>>
55 for VerifyRequest
56where
57 TPayload: Serialize,
58 TRequirements: Serialize,
59{
60 type Error = serde_json::Error;
61 fn try_from(
62 value: TypedVerifyRequest<V, TPayload, TRequirements>,
63 ) -> Result<Self, Self::Error> {
64 let json = serde_json::to_value(value)?;
65 Ok(Self(json))
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct VerifyRequest(serde_json::Value);
72
73impl VerifyRequest {
74 #[must_use]
76 pub fn into_json(self) -> serde_json::Value {
77 self.0
78 }
79
80 #[must_use]
82 pub fn scheme_slug(&self) -> Option<SchemeSlug> {
83 scheme_slug_from_json(&self.0)
84 }
85
86 #[must_use]
88 pub fn network(&self) -> &str {
89 network_from_json(&self.0)
90 }
91}
92
93impl From<serde_json::Value> for VerifyRequest {
94 fn from(value: serde_json::Value) -> Self {
95 Self(value)
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SettleRequest(serde_json::Value);
103
104impl SettleRequest {
105 #[must_use]
107 pub fn into_json(self) -> serde_json::Value {
108 self.0
109 }
110
111 #[must_use]
113 pub fn scheme_slug(&self) -> Option<SchemeSlug> {
114 scheme_slug_from_json(&self.0)
115 }
116
117 #[must_use]
119 pub fn network(&self) -> &str {
120 network_from_json(&self.0)
121 }
122
123 pub fn set_settlement_amount(&mut self, amount: &str) -> Result<(), VerificationError> {
135 let req = self
136 .0
137 .get_mut("paymentRequirements")
138 .and_then(serde_json::Value::as_object_mut)
139 .ok_or_else(|| {
140 VerificationError::InvalidFormat(
141 "settle request missing paymentRequirements object".into(),
142 )
143 })?;
144 let _ = req.insert(
145 "amount".to_owned(),
146 serde_json::Value::String(amount.to_owned()),
147 );
148 Ok(())
149 }
150}
151
152impl From<serde_json::Value> for SettleRequest {
153 fn from(value: serde_json::Value) -> Self {
154 Self(value)
155 }
156}
157
158impl From<VerifyRequest> for SettleRequest {
159 fn from(request: VerifyRequest) -> Self {
160 Self(request.into_json())
161 }
162}
163
164fn scheme_slug_from_json(json: &serde_json::Value) -> Option<SchemeSlug> {
165 let version = json.get("x402Version")?.as_u64()?;
166 let version: u8 = version.try_into().ok()?;
167 if version != Version2::VALUE {
168 return None;
169 }
170 let accepted = json.get("paymentPayload")?.get("accepted")?;
171 let chain_id = ChainId::from_str(accepted.get("network")?.as_str()?).ok()?;
172 let scheme = accepted.get("scheme")?.as_str()?;
173 Some(SchemeSlug::new(chain_id, scheme.into()))
174}
175
176fn network_from_json(json: &serde_json::Value) -> &str {
177 json.get("paymentRequirements")
178 .and_then(|r| r.get("network"))
179 .and_then(serde_json::Value::as_str)
180 .unwrap_or_default()
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 fn v2_json(network: &str, scheme: &str) -> serde_json::Value {
188 serde_json::json!({
189 "x402Version": 2,
190 "paymentPayload": {
191 "accepted": { "network": network, "scheme": scheme }
192 },
193 "paymentRequirements": { "network": network }
194 })
195 }
196
197 #[test]
198 fn verify_request_scheme_slug_evm() {
199 let req = VerifyRequest::from(v2_json("eip155:8453", "exact"));
200 let slug = req.scheme_slug().unwrap();
201 assert_eq!(slug.to_string(), "eip155:8453:exact");
202 }
203
204 #[test]
205 fn settle_request_from_verify_preserves_slug() {
206 let verify = VerifyRequest::from(v2_json("eip155:42161", "exact"));
207 let settle: SettleRequest = verify.into();
208 assert_eq!(
209 settle.scheme_slug().unwrap().to_string(),
210 "eip155:42161:exact"
211 );
212 }
213
214 #[test]
215 fn settle_request_network_missing_returns_empty() {
216 let settle = SettleRequest::from(serde_json::json!({}));
217 assert_eq!(settle.network(), "");
218 }
219
220 #[test]
221 fn slug_rejects_wrong_version() {
222 let mut json = v2_json("eip155:1", "exact");
223 json["x402Version"] = serde_json::json!(99);
224 assert!(scheme_slug_from_json(&json).is_none());
225 }
226
227 #[test]
228 fn slug_rejects_invalid_caip2() {
229 assert!(scheme_slug_from_json(&v2_json("not-a-caip2", "exact")).is_none());
230 }
231
232 #[test]
233 fn settle_amount_override_rewrites_payment_requirements() {
234 let mut settle = SettleRequest::from(serde_json::json!({
235 "x402Version": 2,
236 "paymentPayload": { "accepted": { "network": "eip155:8453", "scheme": "upto" } },
237 "paymentRequirements": { "network": "eip155:8453", "amount": "5000000" }
238 }));
239 settle.set_settlement_amount("1500000").unwrap();
240 let json = settle.into_json();
241 assert_eq!(
242 json["paymentRequirements"]["amount"].as_str(),
243 Some("1500000")
244 );
245 }
246
247 #[test]
248 fn settle_amount_override_errors_when_requirements_missing() {
249 let mut settle = SettleRequest::from(serde_json::json!({}));
250 let err = settle.set_settlement_amount("1").unwrap_err();
251 assert!(matches!(err, VerificationError::InvalidFormat(_)));
252 }
253}