Skip to main content

r402_core/wire/
request.rs

1//! Verify and settle request envelopes.
2
3use 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/// A protocol-versioned verify request parameterized by payload and
13/// requirements types.
14///
15/// The const parameter `V` selects the version marker. Client and
16/// facilitator code that knows the concrete shape decodes a raw
17/// [`VerifyRequest`] into [`TypedVerifyRequest`] via [`Self::from_verify`].
18#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct TypedVerifyRequest<const V: u8, TPayload, TRequirements> {
21    /// Protocol version marker.
22    pub x402_version: super::Version<V>,
23    /// The signed payment authorization.
24    pub payment_payload: TPayload,
25    /// The payment terms being verified.
26    pub payment_requirements: TRequirements,
27}
28
29impl<const V: u8, TPayload, TRequirements> TypedVerifyRequest<V, TPayload, TRequirements>
30where
31    Self: serde::de::DeserializeOwned,
32{
33    /// Decodes a raw [`VerifyRequest`] into this typed variant.
34    ///
35    /// # Errors
36    ///
37    /// Returns [`VerificationError::InvalidFormat`] when deserialisation fails.
38    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    /// Decodes a raw [`SettleRequest`] into this typed variant.
44    ///
45    /// # Errors
46    ///
47    /// Returns [`VerificationError::InvalidFormat`] when deserialisation fails.
48    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/// Wire-level verify request, stored as opaque JSON.
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct VerifyRequest(serde_json::Value);
72
73impl VerifyRequest {
74    /// Consumes the request and returns the raw JSON.
75    #[must_use]
76    pub fn into_json(self) -> serde_json::Value {
77        self.0
78    }
79
80    /// Inspects the request for scheme routing purposes without full decoding.
81    #[must_use]
82    pub fn scheme_slug(&self) -> Option<SchemeSlug> {
83        scheme_slug_from_json(&self.0)
84    }
85
86    /// Returns the CAIP-2 network identifier from `paymentRequirements.network`.
87    #[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/// Wire-level settle request. Identical structure to [`VerifyRequest`] but
100/// distinguished at the type level to prevent accidental misuse.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct SettleRequest(serde_json::Value);
103
104impl SettleRequest {
105    /// Consumes the request and returns the raw JSON.
106    #[must_use]
107    pub fn into_json(self) -> serde_json::Value {
108        self.0
109    }
110
111    /// Inspects the request for scheme routing purposes.
112    #[must_use]
113    pub fn scheme_slug(&self) -> Option<SchemeSlug> {
114        scheme_slug_from_json(&self.0)
115    }
116
117    /// Returns the CAIP-2 network identifier from `paymentRequirements.network`.
118    #[must_use]
119    pub fn network(&self) -> &str {
120        network_from_json(&self.0)
121    }
122
123    /// Overrides `paymentRequirements.amount` in-place.
124    ///
125    /// Intended for the **upto** scheme, where the resource server decides
126    /// the actual settlement amount at request time (≤ the signed maximum).
127    /// For the exact scheme this is a no-op: the amount must already equal
128    /// what the buyer signed.
129    ///
130    /// # Errors
131    ///
132    /// Returns [`VerificationError::InvalidFormat`] when the JSON does not
133    /// have a `paymentRequirements` object.
134    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}