Skip to main content

x402_types/
config.rs

1//! Configuration types for x402 infrastructure.
2//!
3//! This module provides the core configuration types used throughout the x402 ecosystem,
4//! including server configuration, RPC provider configuration, CLI argument parsing,
5//! and environment variable resolution.
6//!
7//! # Overview
8//!
9//! The configuration system is designed to be reusable across different x402 components:
10//!
11//! - [`Config<T>`] - Generic server configuration parameterized by chain config type
12//! - [`CliArgs`] - CLI argument parsing (requires `cli` feature)
13//! - [`LiteralOrEnv`] - Transparent wrapper for environment variable resolution
14//!
15//! # Configuration File Format
16//!
17//! Configuration is loaded from a JSON file (default: `config.json`) with the following structure:
18//!
19//! ```json
20//! {
21//!   "port": 8080,
22//!   "host": "0.0.0.0",
23//!   "chains": { /* chain-specific configuration */ },
24//!   "schemes": [
25//!     { "scheme": "v2-eip155-exact", "chains": ["eip155:8453"] }
26//!   ]
27//! }
28//! ```
29//!
30//! # Environment Variables
31//!
32//! - `CONFIG` - Path to configuration file (default: `config.json`)
33//! - `PORT` - Server port (default: 8080)
34//! - `HOST` - Server bind address (default: `0.0.0.0`)
35//!
36//! # Environment Variable Resolution
37//!
38//! The [`LiteralOrEnv`] wrapper type allows configuration values to be specified
39//! either as literal values or as references to environment variables:
40//!
41//! ```json
42//! {
43//!   "http": "http://localhost:8545",           // Literal value
44//!   "api_key": "$API_KEY",                     // Simple env var
45//!   "secret": "${DATABASE_SECRET}"             // Braced env var
46//! }
47//! ```
48//!
49//! This is particularly useful for keeping secrets out of configuration files
50//! while still allowing them to be loaded at runtime.
51//!
52//! # Feature Flags
53//!
54//! - `cli` - Enables CLI argument parsing via [`clap`]. When enabled, [`Config::load()`]
55//!   parses command-line arguments to determine the config file path.
56
57use serde::{Deserialize, Serialize};
58use std::fmt::{Display, Formatter};
59use std::fs;
60use std::net::IpAddr;
61use std::ops::{Deref, DerefMut};
62use std::path::PathBuf;
63use std::str::FromStr;
64
65#[cfg(feature = "cli")]
66use clap::Parser;
67#[cfg(feature = "cli")]
68use std::path::Path;
69
70use crate::scheme::SchemeConfig;
71
72// ============================================================================
73// Environment Variable Resolution
74// ============================================================================
75
76/// A transparent wrapper that resolves environment variables during deserialization.
77///
78/// Supports both literal values and environment variable references:
79/// - Literal: `"http://localhost:8083"`
80/// - Simple env var: `"$TREASURY_URL"`
81/// - Braced env var: `"${TREASURY_URL}"`
82///
83/// The wrapper implements `Deref` to provide transparent access to the inner type.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct LiteralOrEnv<T>(T, Option<String>);
86
87impl<T> LiteralOrEnv<T> {
88    pub fn from_literal(value: T) -> Self {
89        Self(value, None)
90    }
91
92    /// Get a reference to the inner value
93    #[allow(dead_code)]
94    pub fn inner(&self) -> &T {
95        &self.0
96    }
97
98    /// Consume the wrapper and return the inner value
99    #[allow(dead_code)]
100    pub fn into_inner(self) -> T {
101        self.0
102    }
103
104    /// Parse environment variable syntax from a string.
105    /// Returns the variable name if the string matches `$VAR` or `${VAR}` syntax.
106    fn parse_env_var_syntax(s: &str) -> Option<String> {
107        if s.starts_with("${") && s.ends_with('}') {
108            // ${VAR} syntax
109            Some(s[2..s.len() - 1].to_string())
110        } else if s.starts_with('$') && s.len() > 1 {
111            // $VAR syntax - extract until first non-alphanumeric/underscore character
112            let var_name = &s[1..];
113            if var_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
114                Some(var_name.to_string())
115            } else {
116                None
117            }
118        } else {
119            None
120        }
121    }
122}
123
124impl<T> Deref for LiteralOrEnv<T> {
125    type Target = T;
126
127    fn deref(&self) -> &Self::Target {
128        &self.0
129    }
130}
131
132impl<T> DerefMut for LiteralOrEnv<T> {
133    fn deref_mut(&mut self) -> &mut Self::Target {
134        &mut self.0
135    }
136}
137
138impl<'de, T> Deserialize<'de> for LiteralOrEnv<T>
139where
140    T: FromStr,
141    T::Err: std::fmt::Display,
142{
143    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
144    where
145        D: serde::Deserializer<'de>,
146    {
147        let s = String::deserialize(deserializer)?;
148
149        // Check if it's an environment variable reference
150        let (value, var_name) = if let Some(var_name) = Self::parse_env_var_syntax(&s) {
151            let value = std::env::var(&var_name).map_err(|_| {
152                serde::de::Error::custom(format!(
153                    "Environment variable '{}' not found (referenced as '{}')",
154                    var_name, s
155                ))
156            })?;
157            (value, Some(var_name))
158        } else {
159            (s, None)
160        };
161
162        // Parse the value as type T
163        let parsed = value
164            .parse::<T>()
165            .map_err(|e| serde::de::Error::custom(format!("Failed to parse value: {}", e)))?;
166
167        Ok(LiteralOrEnv(parsed, var_name))
168    }
169}
170
171impl<T> serde::Serialize for LiteralOrEnv<T>
172where
173    T: Serialize,
174{
175    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
176    where
177        S: serde::Serializer,
178    {
179        self.0.serialize(serializer)
180    }
181}
182
183impl<T> Display for LiteralOrEnv<T>
184where
185    T: Display,
186{
187    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
188        match self.1.as_ref() {
189            None => self.0.fmt(f),
190            Some(var_name) => write!(f, "${}", var_name),
191        }
192    }
193}
194
195/// CLI arguments for the x402 facilitator server.
196#[derive(Debug)]
197#[cfg_attr(feature = "cli", derive(Parser))]
198#[cfg_attr(feature = "cli", command(name = "x402-rs"))]
199#[cfg_attr(feature = "cli", command(about = "x402 Facilitator HTTP server"))]
200#[allow(dead_code)] // For downstream crates to use
201pub struct CliArgs {
202    /// Path to the JSON configuration file
203    #[cfg_attr(
204        feature = "cli",
205        arg(long, short, env = "CONFIG", default_value = "config.json")
206    )]
207    pub config: PathBuf,
208}
209
210/// Server configuration.
211///
212/// Fields use serde defaults that fall back to environment variables,
213/// then to hardcoded defaults.
214#[derive(Debug, Clone, Deserialize)]
215pub struct Config<TChainsConfig> {
216    #[serde(default = "config_defaults::default_port")]
217    port: u16,
218    #[serde(default = "config_defaults::default_host")]
219    host: IpAddr,
220    #[serde(default)]
221    chains: TChainsConfig,
222    #[serde(default)]
223    schemes: Vec<SchemeConfig>,
224}
225
226impl<TChainsConfig> Default for Config<TChainsConfig>
227where
228    TChainsConfig: Default,
229{
230    fn default() -> Self {
231        Config {
232            port: config_defaults::default_port(),
233            host: config_defaults::default_host(),
234            chains: TChainsConfig::default(),
235            schemes: Vec::new(),
236        }
237    }
238}
239
240pub mod config_defaults {
241    use std::env;
242    use std::net::IpAddr;
243
244    pub const DEFAULT_PORT: u16 = 8080;
245    pub const DEFAULT_HOST: &str = "0.0.0.0";
246
247    /// Returns the default port value with fallback: $PORT env var -> 8080
248    pub fn default_port() -> u16 {
249        env::var("PORT")
250            .ok()
251            .and_then(|s| s.parse().ok())
252            .unwrap_or(DEFAULT_PORT)
253    }
254
255    /// Returns the default host value with fallback: $HOST env var -> "0.0.0.0"
256    pub fn default_host() -> IpAddr {
257        env::var("HOST")
258            .ok()
259            .and_then(|s| s.parse().ok())
260            .unwrap_or(IpAddr::V4(DEFAULT_HOST.parse().unwrap()))
261    }
262}
263
264impl<TChainsConfig> Config<TChainsConfig> {
265    /// Get the port value.
266    pub fn port(&self) -> u16 {
267        self.port
268    }
269
270    /// Get the host value as an IpAddr.
271    ///
272    /// Returns an error if the host string cannot be parsed as an IP address.
273    pub fn host(&self) -> IpAddr {
274        self.host
275    }
276
277    /// Get the schemes configuration list.
278    ///
279    /// Each entry specifies a scheme and the chains it applies to.
280    pub fn schemes(&self) -> &Vec<SchemeConfig> {
281        &self.schemes
282    }
283
284    /// Get the chains configuration map.
285    ///
286    /// Keys are CAIP-2 chain identifiers (e.g., "eip155:84532", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp").
287    pub fn chains(&self) -> &TChainsConfig {
288        &self.chains
289    }
290}
291
292impl<TChainsConfig> Config<TChainsConfig>
293where
294    TChainsConfig: Default + for<'de> Deserialize<'de>,
295{
296    /// Load configuration from CLI arguments and JSON file.
297    ///
298    /// The config file path is determined by:
299    /// 1. `--config <path>` CLI argument
300    /// 2. `./config.json` (if it exists)
301    ///
302    /// Values not present in the config file will be resolved via
303    /// environment variables or defaults during deserialization.
304    #[cfg(feature = "cli")]
305    pub fn load() -> Result<Self, ConfigError> {
306        let cli_args = CliArgs::parse();
307        let config_path = Path::new(&cli_args.config)
308            .canonicalize()
309            .map_err(|e| ConfigError::FileRead(cli_args.config, e))?;
310        Self::load_from_path(config_path)
311    }
312
313    /// Load configuration from a specific path (or use defaults if None).
314    pub fn load_from_path(path: PathBuf) -> Result<Self, ConfigError> {
315        let content = fs::read_to_string(&path).map_err(|e| ConfigError::FileRead(path, e))?;
316        let config: Config<TChainsConfig> = serde_json::from_str(&content)?;
317        Ok(config)
318    }
319}
320
321/// Configuration error types.
322#[derive(Debug, thiserror::Error)]
323pub enum ConfigError {
324    #[error("Failed to read config file at {0}: {1}")]
325    FileRead(PathBuf, std::io::Error),
326    #[error("Failed to parse config file: {0}")]
327    JsonParse(#[from] serde_json::Error),
328}