Skip to main content

r402_core/wire/
rpc.rs

1//! Facilitator RPC envelopes: verify, settle, and `/supported`.
2
3use std::collections::HashMap;
4use std::str::FromStr;
5
6use compact_str::CompactString;
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use serde_with::{VecSkipError, serde_as};
10use thiserror::Error;
11
12use super::{Base64Bytes, Extensions, Version, Version2};
13use crate::chain::ChainId;
14use crate::error::{AsPaymentProblem, ErrorReason, FacilitatorError, VerificationError};
15use crate::scheme::SchemeSlug;
16
17/// A protocol-versioned verify request parameterized by payload and
18/// requirements types.
19///
20/// The const parameter `V` selects the version marker. Client and
21/// facilitator code that knows the concrete shape decodes a raw
22/// [`VerifyRequest`] into [`TypedVerifyRequest`] via [`Self::from_verify`].
23#[derive(Debug, Clone, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct TypedVerifyRequest<const V: u8, TPayload, TRequirements> {
26    /// Protocol version marker.
27    pub x402_version: Version<V>,
28    /// The signed payment authorization.
29    pub payment_payload: TPayload,
30    /// The payment terms being verified.
31    pub payment_requirements: TRequirements,
32}
33
34impl<const V: u8, TPayload, TRequirements> TypedVerifyRequest<V, TPayload, TRequirements>
35where
36    Self: serde::de::DeserializeOwned,
37{
38    /// Decodes a raw [`VerifyRequest`] into this typed variant.
39    ///
40    /// # Errors
41    ///
42    /// Returns [`VerificationError::InvalidFormat`] when deserialisation fails.
43    pub fn from_verify(request: VerifyRequest) -> Result<Self, VerificationError> {
44        serde_json::from_value(request.into_json())
45            .map_err(|e| VerificationError::InvalidFormat(e.to_string()))
46    }
47
48    /// Decodes a raw [`SettleRequest`] into this typed variant.
49    ///
50    /// # Errors
51    ///
52    /// Returns [`VerificationError::InvalidFormat`] when deserialisation fails.
53    pub fn from_settle(request: SettleRequest) -> Result<Self, VerificationError> {
54        serde_json::from_value(request.into_json())
55            .map_err(|e| VerificationError::InvalidFormat(e.to_string()))
56    }
57}
58
59impl<const V: u8, TPayload, TRequirements> TryFrom<TypedVerifyRequest<V, TPayload, TRequirements>>
60    for VerifyRequest
61where
62    TPayload: Serialize,
63    TRequirements: Serialize,
64{
65    type Error = serde_json::Error;
66    fn try_from(
67        value: TypedVerifyRequest<V, TPayload, TRequirements>,
68    ) -> Result<Self, Self::Error> {
69        let json = serde_json::to_value(value)?;
70        Ok(Self(json))
71    }
72}
73
74/// Wire-level verify request, stored as opaque JSON.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct VerifyRequest(serde_json::Value);
77
78impl VerifyRequest {
79    /// Consumes the request and returns the raw JSON.
80    #[must_use]
81    pub fn into_json(self) -> serde_json::Value {
82        self.0
83    }
84
85    /// Inspects the request for scheme routing purposes without full decoding.
86    #[must_use]
87    pub fn scheme_slug(&self) -> Option<SchemeSlug> {
88        scheme_slug_from_json(&self.0)
89    }
90
91    /// Returns the CAIP-2 network identifier from `paymentRequirements.network`.
92    #[must_use]
93    pub fn network(&self) -> &str {
94        network_from_json(&self.0)
95    }
96}
97
98impl From<serde_json::Value> for VerifyRequest {
99    fn from(value: serde_json::Value) -> Self {
100        Self(value)
101    }
102}
103
104/// Wire-level settle request. Identical structure to [`VerifyRequest`] but
105/// distinguished at the type level to prevent accidental misuse.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct SettleRequest(serde_json::Value);
108
109impl SettleRequest {
110    /// Consumes the request and returns the raw JSON.
111    #[must_use]
112    pub fn into_json(self) -> serde_json::Value {
113        self.0
114    }
115
116    /// Inspects the request for scheme routing purposes.
117    #[must_use]
118    pub fn scheme_slug(&self) -> Option<SchemeSlug> {
119        scheme_slug_from_json(&self.0)
120    }
121
122    /// Returns the CAIP-2 network identifier from `paymentRequirements.network`.
123    #[must_use]
124    pub fn network(&self) -> &str {
125        network_from_json(&self.0)
126    }
127
128    /// Overrides `paymentRequirements.amount` in-place.
129    ///
130    /// Intended for the **upto** scheme, where the resource server decides
131    /// the actual settlement amount at request time (≤ the signed maximum).
132    /// For the exact scheme this is a no-op: the amount must already equal
133    /// what the buyer signed.
134    ///
135    /// # Errors
136    ///
137    /// Returns [`VerificationError::InvalidFormat`] when the JSON does not
138    /// have a `paymentRequirements` object.
139    pub fn set_settlement_amount(&mut self, amount: &str) -> Result<(), VerificationError> {
140        let req = self
141            .0
142            .get_mut("paymentRequirements")
143            .and_then(serde_json::Value::as_object_mut)
144            .ok_or_else(|| {
145                VerificationError::InvalidFormat(
146                    "settle request missing paymentRequirements object".into(),
147                )
148            })?;
149        let _ = req.insert(
150            "amount".to_owned(),
151            serde_json::Value::String(amount.to_owned()),
152        );
153        Ok(())
154    }
155}
156
157impl From<serde_json::Value> for SettleRequest {
158    fn from(value: serde_json::Value) -> Self {
159        Self(value)
160    }
161}
162
163impl From<VerifyRequest> for SettleRequest {
164    fn from(request: VerifyRequest) -> Self {
165        Self(request.into_json())
166    }
167}
168
169fn scheme_slug_from_json(json: &serde_json::Value) -> Option<SchemeSlug> {
170    let version = json.get("x402Version")?.as_u64()?;
171    let version: u8 = version.try_into().ok()?;
172    if version != Version2::VALUE {
173        return None;
174    }
175    let accepted = json.get("paymentPayload")?.get("accepted")?;
176    let chain_id = ChainId::from_str(accepted.get("network")?.as_str()?).ok()?;
177    let scheme = accepted.get("scheme")?.as_str()?;
178    Some(SchemeSlug::new(chain_id, scheme.into()))
179}
180
181fn network_from_json(json: &serde_json::Value) -> &str {
182    json.get("paymentRequirements")
183        .and_then(|r| r.get("network"))
184        .and_then(serde_json::Value::as_str)
185        .unwrap_or_default()
186}
187
188#[cfg(test)]
189mod request_tests {
190    use super::*;
191
192    fn v2_json(network: &str, scheme: &str) -> serde_json::Value {
193        serde_json::json!({
194            "x402Version": 2,
195            "paymentPayload": {
196                "accepted": { "network": network, "scheme": scheme }
197            },
198            "paymentRequirements": { "network": network }
199        })
200    }
201
202    #[test]
203    fn verify_request_scheme_slug_evm() {
204        let req = VerifyRequest::from(v2_json("eip155:8453", "exact"));
205        let slug = req.scheme_slug().unwrap();
206        assert_eq!(slug.to_string(), "eip155:8453:exact");
207    }
208
209    #[test]
210    fn settle_request_from_verify_preserves_slug() {
211        let verify = VerifyRequest::from(v2_json("eip155:42161", "exact"));
212        let settle: SettleRequest = verify.into();
213        assert_eq!(
214            settle.scheme_slug().unwrap().to_string(),
215            "eip155:42161:exact"
216        );
217    }
218
219    #[test]
220    fn settle_request_network_missing_returns_empty() {
221        let settle = SettleRequest::from(serde_json::json!({}));
222        assert_eq!(settle.network(), "");
223    }
224
225    #[test]
226    fn slug_rejects_wrong_version() {
227        let mut json = v2_json("eip155:1", "exact");
228        json["x402Version"] = serde_json::json!(99);
229        assert!(scheme_slug_from_json(&json).is_none());
230    }
231
232    #[test]
233    fn slug_rejects_invalid_caip2() {
234        assert!(scheme_slug_from_json(&v2_json("not-a-caip2", "exact")).is_none());
235    }
236
237    #[test]
238    fn settle_amount_override_rewrites_payment_requirements() {
239        let mut settle = SettleRequest::from(serde_json::json!({
240            "x402Version": 2,
241            "paymentPayload": { "accepted": { "network": "eip155:8453", "scheme": "upto" } },
242            "paymentRequirements": { "network": "eip155:8453", "amount": "5000000" }
243        }));
244        settle.set_settlement_amount("1500000").unwrap();
245        let json = settle.into_json();
246        assert_eq!(
247            json["paymentRequirements"]["amount"].as_str(),
248            Some("1500000")
249        );
250    }
251
252    #[test]
253    fn settle_amount_override_errors_when_requirements_missing() {
254        let mut settle = SettleRequest::from(serde_json::json!({}));
255        let err = settle.set_settlement_amount("1").unwrap_err();
256        assert!(matches!(err, VerificationError::InvalidFormat(_)));
257    }
258}
259
260/// Verification outcome returned by a facilitator.
261///
262/// Serialised as a flat boolean-discriminated JSON object (matching the x402
263/// wire format). Consumers get a safe, pattern-matchable Rust enum.
264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(into = "VerifyResponseWire", try_from = "VerifyResponseWire")]
266#[non_exhaustive]
267pub enum VerifyResponse {
268    /// Payload passed all verification checks.
269    Valid {
270        /// Address of the verified payer.
271        payer: CompactString,
272        /// Facilitator-attached extension data.
273        extensions: Extensions,
274    },
275    /// Payload was well-formed but failed verification.
276    Invalid {
277        /// Wire-level reason code.
278        reason: ErrorReason,
279        /// Optional human-readable message.
280        message: Option<CompactString>,
281        /// Optional payer address if identifiable from the payload.
282        payer: Option<CompactString>,
283        /// Facilitator-attached extension data.
284        extensions: Extensions,
285    },
286}
287
288impl VerifyResponse {
289    /// Convenience: constructs a `Valid` response with no extensions.
290    #[must_use]
291    pub fn valid(payer: impl Into<CompactString>) -> Self {
292        Self::Valid {
293            payer: payer.into(),
294            extensions: Extensions::new(),
295        }
296    }
297
298    /// Convenience: constructs an `Invalid` response without a human message.
299    #[must_use]
300    pub fn invalid(payer: Option<CompactString>, reason: ErrorReason) -> Self {
301        Self::Invalid {
302            reason,
303            message: None,
304            payer,
305            extensions: Extensions::new(),
306        }
307    }
308
309    /// Convenience: constructs an `Invalid` response with a message.
310    #[must_use]
311    pub fn invalid_with_message(
312        payer: Option<CompactString>,
313        reason: ErrorReason,
314        message: impl Into<CompactString>,
315    ) -> Self {
316        Self::Invalid {
317            reason,
318            message: Some(message.into()),
319            payer,
320            extensions: Extensions::new(),
321        }
322    }
323
324    /// Returns `true` for `Valid` outcomes.
325    #[must_use]
326    pub const fn is_valid(&self) -> bool {
327        matches!(self, Self::Valid { .. })
328    }
329
330    /// Converts a [`FacilitatorError`] into an `Invalid` response for the
331    /// HTTP boundary, preserving the structured reason code.
332    #[must_use]
333    pub fn from_facilitator_error(error: &FacilitatorError) -> Self {
334        let problem = error.as_payment_problem();
335        Self::Invalid {
336            reason: problem.reason(),
337            message: Some(CompactString::from(problem.details())),
338            payer: None,
339            extensions: Extensions::new(),
340        }
341    }
342}
343
344/// Flat wire representation of [`VerifyResponse`].
345#[derive(Serialize, Deserialize)]
346#[serde(rename_all = "camelCase", deny_unknown_fields)]
347struct VerifyResponseWire {
348    is_valid: bool,
349    #[serde(default, skip_serializing_if = "Option::is_none")]
350    payer: Option<CompactString>,
351    #[serde(default, skip_serializing_if = "Option::is_none")]
352    invalid_reason: Option<ErrorReason>,
353    #[serde(default, skip_serializing_if = "Option::is_none")]
354    invalid_message: Option<CompactString>,
355    #[serde(default, skip_serializing_if = "Extensions::is_empty")]
356    extensions: Extensions,
357}
358
359impl From<VerifyResponse> for VerifyResponseWire {
360    fn from(value: VerifyResponse) -> Self {
361        match value {
362            VerifyResponse::Valid { payer, extensions } => Self {
363                is_valid: true,
364                payer: Some(payer),
365                invalid_reason: None,
366                invalid_message: None,
367                extensions,
368            },
369            VerifyResponse::Invalid {
370                reason,
371                message,
372                payer,
373                extensions,
374            } => Self {
375                is_valid: false,
376                payer,
377                invalid_reason: Some(reason),
378                invalid_message: message,
379                extensions,
380            },
381        }
382    }
383}
384
385impl TryFrom<VerifyResponseWire> for VerifyResponse {
386    type Error = String;
387    fn try_from(wire: VerifyResponseWire) -> Result<Self, Self::Error> {
388        if wire.is_valid {
389            Ok(Self::Valid {
390                payer: wire.payer.ok_or("missing field: payer")?,
391                extensions: wire.extensions,
392            })
393        } else {
394            Ok(Self::Invalid {
395                reason: wire.invalid_reason.ok_or("missing field: invalidReason")?,
396                message: wire.invalid_message,
397                payer: wire.payer,
398                extensions: wire.extensions,
399            })
400        }
401    }
402}
403
404/// Settlement outcome returned by a facilitator.
405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
406#[serde(into = "SettleResponseWire", try_from = "SettleResponseWire")]
407#[non_exhaustive]
408pub enum SettleResponse {
409    /// Settlement succeeded.
410    Success {
411        /// The payer address on the target chain.
412        payer: CompactString,
413        /// On-chain transaction hash / signature.
414        transaction: CompactString,
415        /// CAIP-2 chain identifier the transaction landed on.
416        network: CompactString,
417        /// Actual amount settled, in the token's smallest unit.
418        ///
419        /// Required when the `upto` scheme is in use; included for `exact` too
420        /// to give clients unambiguous receipts.
421        #[serde(default, skip_serializing_if = "Option::is_none")]
422        amount: Option<CompactString>,
423        /// Facilitator-attached extension data.
424        extensions: Extensions,
425    },
426    /// Settlement failed.
427    Failure {
428        /// Wire-level reason code.
429        reason: ErrorReason,
430        /// Optional human-readable message.
431        message: Option<CompactString>,
432        /// Optional payer address if identifiable.
433        payer: Option<CompactString>,
434        /// CAIP-2 chain identifier on which settlement was attempted.
435        network: CompactString,
436        /// Facilitator-attached extension data.
437        extensions: Extensions,
438    },
439}
440
441impl SettleResponse {
442    /// Returns `true` when the settlement succeeded.
443    #[must_use]
444    pub const fn is_success(&self) -> bool {
445        matches!(self, Self::Success { .. })
446    }
447
448    /// Encodes a successful settlement as base64 bytes for the
449    /// `Payment-Response` HTTP header.
450    ///
451    /// Returns `None` for `Failure` variants to avoid accidentally
452    /// transmitting a failed settlement as if it were successful. Call
453    /// [`Self::encode_base64_any`] when the failure body is needed on the
454    /// wire (for example, when a paygate must inform a browser client of a
455    /// failed settlement via the `Payment-Response` header).
456    #[must_use]
457    pub fn encode_base64(&self) -> Option<Base64Bytes> {
458        if !self.is_success() {
459            return None;
460        }
461        let json = serde_json::to_vec(self).ok()?;
462        Some(Base64Bytes::encode(json))
463    }
464
465    /// Encodes any [`SettleResponse`] (success or failure) as base64 bytes
466    /// for the `Payment-Response` HTTP header.
467    ///
468    /// Use this when surfacing failed settlements is required by the spec,
469    /// e.g. paygate error paths that still want to communicate the
470    /// machine-readable error reason and chain to a browser client.
471    /// Prefer [`Self::encode_base64`] when you only want to forward
472    /// successful settlements.
473    #[must_use]
474    pub fn encode_base64_any(&self) -> Option<Base64Bytes> {
475        let json = serde_json::to_vec(self).ok()?;
476        Some(Base64Bytes::encode(json))
477    }
478
479    /// Builds a `Failure` response from a [`FacilitatorError`].
480    #[must_use]
481    pub fn from_facilitator_error(
482        error: &FacilitatorError,
483        network: impl Into<CompactString>,
484    ) -> Self {
485        let problem = error.as_payment_problem();
486        Self::Failure {
487            reason: problem.reason(),
488            message: Some(CompactString::from(problem.details())),
489            payer: None,
490            network: network.into(),
491            extensions: Extensions::new(),
492        }
493    }
494}
495
496/// Flat wire representation of [`SettleResponse`].
497#[derive(Serialize, Deserialize)]
498#[serde(rename_all = "camelCase", deny_unknown_fields)]
499struct SettleResponseWire {
500    success: bool,
501    #[serde(default, skip_serializing_if = "Option::is_none")]
502    error_reason: Option<ErrorReason>,
503    #[serde(default, skip_serializing_if = "Option::is_none")]
504    error_message: Option<CompactString>,
505    #[serde(default, skip_serializing_if = "Option::is_none")]
506    payer: Option<CompactString>,
507    /// Always serialized per x402 v2 §5.3.2: empty string on failure, and
508    /// empty string on success for off-chain / deferred schemes (e.g.
509    /// batch-settlement vouchers). On-chain schemes set a transaction hash.
510    #[serde(default)]
511    transaction: CompactString,
512    network: CompactString,
513    #[serde(default, skip_serializing_if = "Option::is_none")]
514    amount: Option<CompactString>,
515    #[serde(default, skip_serializing_if = "Extensions::is_empty")]
516    extensions: Extensions,
517}
518
519impl From<SettleResponse> for SettleResponseWire {
520    fn from(value: SettleResponse) -> Self {
521        match value {
522            SettleResponse::Success {
523                payer,
524                transaction,
525                network,
526                amount,
527                extensions,
528            } => Self {
529                success: true,
530                error_reason: None,
531                error_message: None,
532                payer: Some(payer),
533                transaction,
534                network,
535                amount,
536                extensions,
537            },
538            SettleResponse::Failure {
539                reason,
540                message,
541                payer,
542                network,
543                extensions,
544            } => Self {
545                success: false,
546                error_reason: Some(reason),
547                error_message: message,
548                payer,
549                transaction: CompactString::default(),
550                network,
551                amount: None,
552                extensions,
553            },
554        }
555    }
556}
557
558impl TryFrom<SettleResponseWire> for SettleResponse {
559    type Error = String;
560    fn try_from(wire: SettleResponseWire) -> Result<Self, Self::Error> {
561        if wire.success {
562            let payer = wire.payer.ok_or("missing field: payer")?;
563            Ok(Self::Success {
564                payer,
565                transaction: wire.transaction,
566                network: wire.network,
567                amount: wire.amount,
568                extensions: wire.extensions,
569            })
570        } else {
571            Ok(Self::Failure {
572                reason: wire.error_reason.ok_or("missing field: errorReason")?,
573                message: wire.error_message,
574                payer: wire.payer,
575                network: wire.network,
576                extensions: wire.extensions,
577            })
578        }
579    }
580}
581
582#[cfg(test)]
583mod response_tests {
584    use serde_json::json;
585
586    use super::*;
587    use crate::wire::ExtensionEntry;
588
589    #[test]
590    fn verify_valid_roundtrip() {
591        let response = VerifyResponse::valid("0xABC");
592        let encoded = serde_json::to_value(&response).unwrap();
593        assert_eq!(encoded["isValid"], true);
594        assert_eq!(encoded["payer"], "0xABC");
595        assert!(encoded.get("invalidReason").is_none());
596
597        let back: VerifyResponse = serde_json::from_value(encoded).unwrap();
598        assert_eq!(back, response);
599    }
600
601    #[test]
602    fn verify_valid_with_extensions_roundtrip() {
603        let mut extensions = Extensions::new();
604        extensions.insert(
605            "payment-identifier",
606            ExtensionEntry::info(json!({"id": "order-123"})),
607        );
608        let response = VerifyResponse::Valid {
609            payer: "0xABC".into(),
610            extensions,
611        };
612        let encoded = serde_json::to_value(&response).unwrap();
613        assert_eq!(
614            encoded["extensions"]["payment-identifier"]["info"]["id"],
615            "order-123"
616        );
617        let back: VerifyResponse = serde_json::from_value(encoded).unwrap();
618        assert_eq!(back, response);
619    }
620
621    #[test]
622    fn verify_invalid_roundtrip() {
623        let response = VerifyResponse::invalid_with_message(
624            Some("0xDEF".into()),
625            ErrorReason::InsufficientFunds,
626            "not enough USDC",
627        );
628        let encoded = serde_json::to_value(&response).unwrap();
629        assert_eq!(encoded["isValid"], false);
630        assert_eq!(encoded["invalidReason"], "insufficient_funds");
631        assert_eq!(encoded["invalidMessage"], "not enough USDC");
632        let back: VerifyResponse = serde_json::from_value(encoded).unwrap();
633        assert_eq!(back, response);
634    }
635
636    #[test]
637    fn settle_success_with_amount_roundtrip() {
638        let response = SettleResponse::Success {
639            payer: "0xABC".into(),
640            transaction: "0xTX".into(),
641            network: "eip155:8453".into(),
642            amount: Some("1000000".into()),
643            extensions: Extensions::new(),
644        };
645        let encoded = serde_json::to_value(&response).unwrap();
646        assert_eq!(encoded["success"], true);
647        assert_eq!(encoded["amount"], "1000000");
648        let back: SettleResponse = serde_json::from_value(encoded).unwrap();
649        assert_eq!(back, response);
650    }
651
652    #[test]
653    fn settle_success_without_amount_is_valid() {
654        let response = SettleResponse::Success {
655            payer: "0xABC".into(),
656            transaction: "0xTX".into(),
657            network: "eip155:1".into(),
658            amount: None,
659            extensions: Extensions::new(),
660        };
661        let encoded = serde_json::to_value(&response).unwrap();
662        assert!(encoded.get("amount").is_none());
663        let back: SettleResponse = serde_json::from_value(encoded).unwrap();
664        assert_eq!(back, response);
665    }
666
667    #[test]
668    fn settle_success_empty_transaction_allowed() {
669        // Off-chain / deferred schemes (batch-settlement vouchers) use "".
670        let json = json!({
671            "success": true,
672            "payer": "0xABC",
673            "transaction": "",
674            "network": "eip155:1"
675        });
676        let back: SettleResponse = serde_json::from_value(json).unwrap();
677        assert!(matches!(
678            back,
679            SettleResponse::Success { ref transaction, .. } if transaction.is_empty()
680        ));
681    }
682
683    #[test]
684    fn settle_failure_roundtrip() {
685        let response = SettleResponse::Failure {
686            reason: ErrorReason::DuplicateSettlement,
687            message: Some("already processed".into()),
688            payer: None,
689            network: "solana:mainnet".into(),
690            extensions: Extensions::new(),
691        };
692        let encoded = serde_json::to_value(&response).unwrap();
693        assert_eq!(encoded["errorReason"], "duplicate_settlement");
694        let back: SettleResponse = serde_json::from_value(encoded).unwrap();
695        assert_eq!(back, response);
696    }
697
698    /// Regression test for F-002: spec §5.3.2 marks `transaction` as Required
699    /// (empty string on failure). Go SDK rejects responses lacking the field.
700    #[test]
701    fn settle_failure_serializes_empty_transaction() {
702        let response = SettleResponse::Failure {
703            reason: ErrorReason::UnexpectedSettleError,
704            message: None,
705            payer: None,
706            network: "eip155:8453".into(),
707            extensions: Extensions::new(),
708        };
709        let encoded = serde_json::to_value(&response).unwrap();
710        assert_eq!(
711            encoded["transaction"], "",
712            "spec §5.3.2 requires `transaction` field present (empty on failure)"
713        );
714    }
715
716    #[test]
717    fn settle_encode_base64_only_for_success() {
718        let success = SettleResponse::Success {
719            payer: "0xA".into(),
720            transaction: "0xT".into(),
721            network: "eip155:1".into(),
722            amount: None,
723            extensions: Extensions::new(),
724        };
725        assert!(success.encode_base64().is_some());
726
727        let failure = SettleResponse::Failure {
728            reason: ErrorReason::UnexpectedSettleError,
729            message: None,
730            payer: None,
731            network: "eip155:1".into(),
732            extensions: Extensions::new(),
733        };
734        assert!(failure.encode_base64().is_none());
735    }
736}
737
738/// A single payment kind advertised by a facilitator.
739#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
740#[serde(rename_all = "camelCase", deny_unknown_fields)]
741#[non_exhaustive]
742pub struct SupportedPaymentKind {
743    /// x402 protocol version (`2`).
744    pub x402_version: u8,
745    /// Scheme name (e.g. `"exact"`, `"upto"`).
746    pub scheme: CompactString,
747    /// CAIP-2 network identifier.
748    pub network: CompactString,
749    /// Optional scheme-specific extras (fee payer, memo, ...).
750    #[serde(default, skip_serializing_if = "Option::is_none")]
751    pub extra: Option<serde_json::Value>,
752}
753
754impl SupportedPaymentKind {
755    /// Constructs a kind from the three required fields. Use [`Self::with_extra`]
756    /// to attach scheme-specific extras (fee payer, memo, etc.).
757    #[must_use]
758    pub fn new(
759        x402_version: u8,
760        scheme: impl Into<CompactString>,
761        network: impl Into<CompactString>,
762    ) -> Self {
763        Self {
764            x402_version,
765            scheme: scheme.into(),
766            network: network.into(),
767            extra: None,
768        }
769    }
770
771    /// Builder: attaches an `extra` JSON blob.
772    #[must_use]
773    pub fn with_extra(mut self, extra: serde_json::Value) -> Self {
774        self.extra = Some(extra);
775        self
776    }
777
778    /// Builder: attaches an optional `extra` blob, useful when the value is
779    /// produced via `Option::map` upstream.
780    #[must_use]
781    pub fn with_optional_extra(mut self, extra: Option<serde_json::Value>) -> Self {
782        self.extra = extra;
783        self
784    }
785}
786
787/// Response body of a facilitator's `/supported` endpoint.
788///
789/// Describes the full set of capabilities: payment kinds, known extensions,
790/// and signer addresses keyed by CAIP-2 chain pattern.
791#[serde_as]
792#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
793#[serde(rename_all = "camelCase", deny_unknown_fields)]
794#[non_exhaustive]
795pub struct SupportedResponse {
796    /// Supported payment kinds. Invalid entries are silently skipped.
797    #[serde_as(as = "VecSkipError<_>")]
798    pub kinds: Vec<SupportedPaymentKind>,
799    /// Supported extension identifiers.
800    #[serde(default)]
801    pub extensions: Vec<CompactString>,
802    /// Signer addresses indexed by CAIP-2 pattern
803    /// (`"eip155:8453"`, `"solana:*"`, ...).
804    #[serde(default)]
805    pub signers: HashMap<CompactString, Vec<CompactString>>,
806}
807
808impl SupportedResponse {
809    /// Constructs an empty response. Equivalent to [`Default::default`] but
810    /// recommended for explicit construction sites where the field set will
811    /// grow over time.
812    #[must_use]
813    pub fn new() -> Self {
814        Self::default()
815    }
816
817    /// Builder: replaces the `kinds` list.
818    #[must_use]
819    pub fn with_kinds(mut self, kinds: Vec<SupportedPaymentKind>) -> Self {
820        self.kinds = kinds;
821        self
822    }
823
824    /// Builder: replaces the `extensions` identifier list.
825    #[must_use]
826    pub fn with_extensions(mut self, extensions: Vec<CompactString>) -> Self {
827        self.extensions = extensions;
828        self
829    }
830
831    /// Builder: replaces the per-pattern signer map.
832    #[must_use]
833    pub fn with_signers(mut self, signers: HashMap<CompactString, Vec<CompactString>>) -> Self {
834        self.signers = signers;
835        self
836    }
837
838    /// Returns all signer addresses that match the given chain.
839    ///
840    /// Matches both the exact pattern (`"eip155:8453"`) and the namespace
841    /// wildcard (`"eip155:*"`).
842    #[must_use]
843    pub fn signers_for_chain(&self, chain_id: &ChainId) -> Vec<&str> {
844        let exact = CompactString::from(chain_id.to_string());
845        let wildcard = CompactString::from(format!("{}:*", chain_id.namespace()));
846        let mut out = Vec::new();
847        if let Some(list) = self.signers.get(&exact) {
848            out.extend(list.iter().map(CompactString::as_str));
849        }
850        if let Some(list) = self.signers.get(&wildcard) {
851            out.extend(list.iter().map(CompactString::as_str));
852        }
853        out
854    }
855}
856
857/// Official wire shape for partial settlement (upto billing by actual usage).
858#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
859#[serde(rename_all = "camelCase")]
860#[non_exhaustive]
861pub struct SettlementOverrides {
862    /// Amount to settle. Formats: atomic `"1000"`, percent `"50%"`, dollar `"$0.05"`.
863    #[serde(default, skip_serializing_if = "Option::is_none")]
864    pub amount: Option<String>,
865}
866
867impl SettlementOverrides {
868    /// Builds overrides with a single amount string.
869    #[must_use]
870    pub fn amount(amount: impl Into<String>) -> Self {
871        Self {
872            amount: Some(amount.into()),
873        }
874    }
875}
876
877/// Failure resolving a settlement override amount string.
878#[derive(Debug, Clone, PartialEq, Eq, Error)]
879pub enum SettlementOverrideError {
880    /// Authorized maximum is not a base-10 integer.
881    #[error("invalid requirements amount: {0}")]
882    InvalidRequirementsAmount(String),
883    /// Percent or dollar override failed to parse / convert.
884    #[error("invalid settlement override amount: {0}")]
885    InvalidOverride(String),
886    /// Intermediate arithmetic overflowed.
887    #[error("settlement override arithmetic overflow")]
888    Overflow,
889}
890
891/// Default ERC-20 style decimals when `extra.decimals` is absent (matches Go).
892pub const DEFAULT_ASSET_DECIMALS: u32 = 6;
893
894/// Reads `decimals` from payment-requirements `extra` (number or numeric string).
895///
896/// Falls back to [`DEFAULT_ASSET_DECIMALS`].
897#[must_use]
898pub fn asset_decimals_from_extra(extra: Option<&serde_json::Value>) -> u32 {
899    let Some(extra) = extra else {
900        return DEFAULT_ASSET_DECIMALS;
901    };
902    match extra.get("decimals") {
903        Some(serde_json::Value::Number(n)) => n
904            .as_u64()
905            .and_then(|v| u32::try_from(v).ok())
906            .unwrap_or(DEFAULT_ASSET_DECIMALS),
907        Some(serde_json::Value::String(s)) => s.parse().unwrap_or(DEFAULT_ASSET_DECIMALS),
908        _ => DEFAULT_ASSET_DECIMALS,
909    }
910}
911
912/// Resolves a settlement override amount to atomic units (decimal string).
913///
914/// # Errors
915///
916/// Returns [`SettlementOverrideError`] when the override or authorized max
917/// cannot be parsed, or intermediate arithmetic overflows `u128`.
918pub fn resolve_settlement_override_amount(
919    raw_amount: &str,
920    authorized_max: &str,
921    decimals: u32,
922) -> Result<String, SettlementOverrideError> {
923    let raw = raw_amount.trim();
924    if let Some(percent_body) = raw.strip_suffix('%') {
925        return resolve_percent(percent_body.trim(), authorized_max);
926    }
927    if let Some(dollar_body) = raw.strip_prefix('$') {
928        return resolve_dollar(dollar_body.trim(), decimals);
929    }
930    // Raw atomic units: pass through after validating base-10 digits.
931    if raw.is_empty() || !raw.bytes().all(|b| b.is_ascii_digit()) {
932        return Err(SettlementOverrideError::InvalidOverride(raw.to_owned()));
933    }
934    Ok(raw.to_owned())
935}
936
937fn resolve_percent(percent: &str, authorized_max: &str) -> Result<String, SettlementOverrideError> {
938    // Up to 2 decimal places: "50" / "50.5" / "50.25" → scaled hundredths of a percent.
939    let (int_part, frac_part) = match percent.split_once('.') {
940        Some((i, f)) => (i, f),
941        None => (percent, ""),
942    };
943    if int_part.is_empty()
944        || !int_part.bytes().all(|b| b.is_ascii_digit())
945        || frac_part.len() > 2
946        || !frac_part.bytes().all(|b| b.is_ascii_digit())
947    {
948        return Err(SettlementOverrideError::InvalidOverride(format!(
949            "{percent}%"
950        )));
951    }
952    let int_val: u128 = int_part
953        .parse()
954        .map_err(|_| SettlementOverrideError::InvalidOverride(format!("{percent}%")))?;
955    let frac_padded = format!("{frac_part:0<2}");
956    let frac_val: u128 = if frac_padded.is_empty() {
957        0
958    } else {
959        frac_padded
960            .parse()
961            .map_err(|_| SettlementOverrideError::InvalidOverride(format!("{percent}%")))?
962    };
963    // scaledPercent = int*100 + frac  (percent with 2 d.p. × 100)
964    let scaled_percent = int_val
965        .checked_mul(100)
966        .and_then(|v| v.checked_add(frac_val))
967        .ok_or(SettlementOverrideError::Overflow)?;
968
969    let base = parse_u128_amount(authorized_max)?;
970    // result = base * scaledPercent / 10000
971    let product = base
972        .checked_mul(scaled_percent)
973        .ok_or(SettlementOverrideError::Overflow)?;
974    Ok((product / 10_000).to_string())
975}
976
977fn resolve_dollar(dollar: &str, decimals: u32) -> Result<String, SettlementOverrideError> {
978    let dollar_dec = Decimal::from_str(dollar)
979        .map_err(|_| SettlementOverrideError::InvalidOverride(format!("${dollar}")))?;
980    if dollar_dec.is_sign_negative() {
981        return Err(SettlementOverrideError::InvalidOverride(format!(
982            "${dollar}"
983        )));
984    }
985    let scale = Decimal::from(10u64.pow(decimals.min(18)));
986    let atomic = dollar_dec
987        .checked_mul(scale)
988        .ok_or(SettlementOverrideError::Overflow)?;
989    // Floor toward zero for positive values (matches Go big.Float.Int).
990    let truncated = atomic.trunc();
991    let s = truncated.normalize().to_string();
992    // Decimal may emit "1000.0" — strip fractional part if present after trunc.
993    Ok(s.split('.').next().unwrap_or("0").to_owned())
994}
995
996fn parse_u128_amount(raw: &str) -> Result<u128, SettlementOverrideError> {
997    if raw.is_empty() || !raw.bytes().all(|b| b.is_ascii_digit()) {
998        return Err(SettlementOverrideError::InvalidRequirementsAmount(
999            raw.to_owned(),
1000        ));
1001    }
1002    raw.parse()
1003        .map_err(|_| SettlementOverrideError::InvalidRequirementsAmount(raw.to_owned()))
1004}
1005
1006#[cfg(test)]
1007mod override_tests {
1008    use super::*;
1009
1010    #[test]
1011    fn atomic_passthrough() {
1012        assert_eq!(
1013            resolve_settlement_override_amount("500", "1000000", 6).unwrap(),
1014            "500"
1015        );
1016    }
1017
1018    #[test]
1019    fn percent_half() {
1020        assert_eq!(
1021            resolve_settlement_override_amount("50%", "1000000", 6).unwrap(),
1022            "500000"
1023        );
1024    }
1025
1026    #[test]
1027    fn percent_with_fraction() {
1028        // 12.5% of 1000 = 125
1029        assert_eq!(
1030            resolve_settlement_override_amount("12.5%", "1000", 6).unwrap(),
1031            "125"
1032        );
1033    }
1034
1035    #[test]
1036    fn dollar_default_decimals() {
1037        assert_eq!(
1038            resolve_settlement_override_amount("$0.001", "1000000", 6).unwrap(),
1039            "1000"
1040        );
1041    }
1042
1043    #[test]
1044    fn rejects_empty_atomic() {
1045        assert!(resolve_settlement_override_amount("", "1", 6).is_err());
1046    }
1047
1048    #[test]
1049    fn decimals_from_extra() {
1050        let v = serde_json::json!({"decimals": 18});
1051        assert_eq!(asset_decimals_from_extra(Some(&v)), 18);
1052        assert_eq!(asset_decimals_from_extra(None), 6);
1053    }
1054}