Skip to main content

r402_core/wire/
supported.rs

1//! Facilitator capability discovery.
2
3use std::collections::HashMap;
4
5use compact_str::CompactString;
6use serde::{Deserialize, Serialize};
7use serde_with::{VecSkipError, serde_as};
8
9use crate::chain::ChainId;
10
11/// A single payment kind advertised by a facilitator.
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase", deny_unknown_fields)]
14#[non_exhaustive]
15pub struct SupportedPaymentKind {
16    /// x402 protocol version (`2`).
17    pub x402_version: u8,
18    /// Scheme name (e.g. `"exact"`, `"upto"`).
19    pub scheme: CompactString,
20    /// CAIP-2 network identifier.
21    pub network: CompactString,
22    /// Optional scheme-specific extras (fee payer, memo, ...).
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub extra: Option<serde_json::Value>,
25}
26
27impl SupportedPaymentKind {
28    /// Constructs a kind from the three required fields. Use [`Self::with_extra`]
29    /// to attach scheme-specific extras (fee payer, memo, etc.).
30    #[must_use]
31    pub fn new(
32        x402_version: u8,
33        scheme: impl Into<CompactString>,
34        network: impl Into<CompactString>,
35    ) -> Self {
36        Self {
37            x402_version,
38            scheme: scheme.into(),
39            network: network.into(),
40            extra: None,
41        }
42    }
43
44    /// Builder: attaches an `extra` JSON blob.
45    #[must_use]
46    pub fn with_extra(mut self, extra: serde_json::Value) -> Self {
47        self.extra = Some(extra);
48        self
49    }
50
51    /// Builder: attaches an optional `extra` blob, useful when the value is
52    /// produced via `Option::map` upstream.
53    #[must_use]
54    pub fn with_optional_extra(mut self, extra: Option<serde_json::Value>) -> Self {
55        self.extra = extra;
56        self
57    }
58}
59
60/// Response body of a facilitator's `/supported` endpoint.
61///
62/// Describes the full set of capabilities: payment kinds, known extensions,
63/// and signer addresses keyed by CAIP-2 chain pattern.
64#[serde_as]
65#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase", deny_unknown_fields)]
67#[non_exhaustive]
68pub struct SupportedResponse {
69    /// Supported payment kinds. Invalid entries are silently skipped.
70    #[serde_as(as = "VecSkipError<_>")]
71    pub kinds: Vec<SupportedPaymentKind>,
72    /// Supported extension identifiers.
73    #[serde(default)]
74    pub extensions: Vec<CompactString>,
75    /// Signer addresses indexed by CAIP-2 pattern
76    /// (`"eip155:8453"`, `"solana:*"`, ...).
77    #[serde(default)]
78    pub signers: HashMap<CompactString, Vec<CompactString>>,
79}
80
81impl SupportedResponse {
82    /// Constructs an empty response. Equivalent to [`Default::default`] but
83    /// recommended for explicit construction sites where the field set will
84    /// grow over time.
85    #[must_use]
86    pub fn new() -> Self {
87        Self::default()
88    }
89
90    /// Builder: replaces the `kinds` list.
91    #[must_use]
92    pub fn with_kinds(mut self, kinds: Vec<SupportedPaymentKind>) -> Self {
93        self.kinds = kinds;
94        self
95    }
96
97    /// Builder: replaces the `extensions` identifier list.
98    #[must_use]
99    pub fn with_extensions(mut self, extensions: Vec<CompactString>) -> Self {
100        self.extensions = extensions;
101        self
102    }
103
104    /// Builder: replaces the per-pattern signer map.
105    #[must_use]
106    pub fn with_signers(mut self, signers: HashMap<CompactString, Vec<CompactString>>) -> Self {
107        self.signers = signers;
108        self
109    }
110
111    /// Returns all signer addresses that match the given chain.
112    ///
113    /// Matches both the exact pattern (`"eip155:8453"`) and the namespace
114    /// wildcard (`"eip155:*"`).
115    #[must_use]
116    pub fn signers_for_chain(&self, chain_id: &ChainId) -> Vec<&str> {
117        let exact = CompactString::from(chain_id.to_string());
118        let wildcard = CompactString::from(format!("{}:*", chain_id.namespace()));
119        let mut out = Vec::new();
120        if let Some(list) = self.signers.get(&exact) {
121            out.extend(list.iter().map(CompactString::as_str));
122        }
123        if let Some(list) = self.signers.get(&wildcard) {
124            out.extend(list.iter().map(CompactString::as_str));
125        }
126        out
127    }
128}