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::fs;
59use std::net::IpAddr;
60use std::ops::{Deref, DerefMut};
61use std::path::PathBuf;
62use std::str::FromStr;
63
64#[cfg(feature = "cli")]
65use clap::Parser;
66#[cfg(feature = "cli")]
67use std::path::Path;
68
69use crate::scheme::SchemeConfig;
70
71// ============================================================================
72// Environment Variable Resolution
73// ============================================================================
74
75/// A transparent wrapper that resolves environment variables during deserialization.
76///
77/// Supports both literal values and environment variable references:
78/// - Literal: `"http://localhost:8083"`
79/// - Simple env var: `"$TREASURY_URL"`
80/// - Braced env var: `"${TREASURY_URL}"`
81///
82/// The wrapper implements `Deref` to provide transparent access to the inner type.
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct LiteralOrEnv<T>(T);
85
86impl<T> LiteralOrEnv<T> {
87    pub fn from_literal(value: T) -> Self {
88        Self(value)
89    }
90
91    /// Get a reference to the inner value
92    #[allow(dead_code)]
93    pub fn inner(&self) -> &T {
94        &self.0
95    }
96
97    /// Consume the wrapper and return the inner value
98    #[allow(dead_code)]
99    pub fn into_inner(self) -> T {
100        self.0
101    }
102
103    /// Parse environment variable syntax from a string.
104    /// Returns the variable name if the string matches `$VAR` or `${VAR}` syntax.
105    fn parse_env_var_syntax(s: &str) -> Option<String> {
106        if s.starts_with("${") && s.ends_with('}') {
107            // ${VAR} syntax
108            Some(s[2..s.len() - 1].to_string())
109        } else if s.starts_with('$') && s.len() > 1 {
110            // $VAR syntax - extract until first non-alphanumeric/underscore character
111            let var_name = &s[1..];
112            if var_name.chars().all(|c| c.is_alphanumeric() || c == '_') {
113                Some(var_name.to_string())
114            } else {
115                None
116            }
117        } else {
118            None
119        }
120    }
121}
122
123impl<T> Deref for LiteralOrEnv<T> {
124    type Target = T;
125
126    fn deref(&self) -> &Self::Target {
127        &self.0
128    }
129}
130
131impl<T> DerefMut for LiteralOrEnv<T> {
132    fn deref_mut(&mut self) -> &mut Self::Target {
133        &mut self.0
134    }
135}
136
137impl<'de, T> Deserialize<'de> for LiteralOrEnv<T>
138where
139    T: FromStr,
140    T::Err: std::fmt::Display,
141{
142    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
143    where
144        D: serde::Deserializer<'de>,
145    {
146        let s = String::deserialize(deserializer)?;
147
148        // Check if it's an environment variable reference
149        let value = if let Some(var_name) = Self::parse_env_var_syntax(&s) {
150            std::env::var(&var_name).map_err(|_| {
151                serde::de::Error::custom(format!(
152                    "Environment variable '{}' not found (referenced as '{}')",
153                    var_name, s
154                ))
155            })?
156        } else {
157            s
158        };
159
160        // Parse the value as type T
161        let parsed = value
162            .parse::<T>()
163            .map_err(|e| serde::de::Error::custom(format!("Failed to parse value: {}", e)))?;
164
165        Ok(LiteralOrEnv(parsed))
166    }
167}
168
169impl<T> serde::Serialize for LiteralOrEnv<T>
170where
171    T: Serialize,
172{
173    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
174    where
175        S: serde::Serializer,
176    {
177        self.0.serialize(serializer)
178    }
179}
180
181/// CLI arguments for the x402 facilitator server.
182#[derive(Debug)]
183#[cfg_attr(feature = "cli", derive(Parser))]
184#[cfg_attr(feature = "cli", command(name = "x402-rs"))]
185#[cfg_attr(feature = "cli", command(about = "x402 Facilitator HTTP server"))]
186#[allow(dead_code)] // For downstream crates to use
187pub struct CliArgs {
188    /// Path to the JSON configuration file
189    #[cfg_attr(
190        feature = "cli",
191        arg(long, short, env = "CONFIG", default_value = "config.json")
192    )]
193    pub config: PathBuf,
194}
195
196/// Server configuration.
197///
198/// Fields use serde defaults that fall back to environment variables,
199/// then to hardcoded defaults.
200#[derive(Debug, Clone, Deserialize)]
201pub struct Config<TChainsConfig> {
202    #[serde(default = "config_defaults::default_port")]
203    port: u16,
204    #[serde(default = "config_defaults::default_host")]
205    host: IpAddr,
206    #[serde(default)]
207    chains: TChainsConfig,
208    #[serde(default)]
209    schemes: Vec<SchemeConfig>,
210}
211
212impl<TChainsConfig> Default for Config<TChainsConfig>
213where
214    TChainsConfig: Default,
215{
216    fn default() -> Self {
217        Config {
218            port: config_defaults::default_port(),
219            host: config_defaults::default_host(),
220            chains: TChainsConfig::default(),
221            schemes: Vec::new(),
222        }
223    }
224}
225
226pub mod config_defaults {
227    use std::env;
228    use std::net::IpAddr;
229
230    pub const DEFAULT_PORT: u16 = 8080;
231    pub const DEFAULT_HOST: &str = "0.0.0.0";
232
233    /// Returns the default port value with fallback: $PORT env var -> 8080
234    pub fn default_port() -> u16 {
235        env::var("PORT")
236            .ok()
237            .and_then(|s| s.parse().ok())
238            .unwrap_or(DEFAULT_PORT)
239    }
240
241    /// Returns the default host value with fallback: $HOST env var -> "0.0.0.0"
242    pub fn default_host() -> IpAddr {
243        env::var("HOST")
244            .ok()
245            .and_then(|s| s.parse().ok())
246            .unwrap_or(IpAddr::V4(DEFAULT_HOST.parse().unwrap()))
247    }
248}
249
250impl<TChainsConfig> Config<TChainsConfig> {
251    /// Get the port value.
252    pub fn port(&self) -> u16 {
253        self.port
254    }
255
256    /// Get the host value as an IpAddr.
257    ///
258    /// Returns an error if the host string cannot be parsed as an IP address.
259    pub fn host(&self) -> IpAddr {
260        self.host
261    }
262
263    /// Get the schemes configuration list.
264    ///
265    /// Each entry specifies a scheme and the chains it applies to.
266    pub fn schemes(&self) -> &Vec<SchemeConfig> {
267        &self.schemes
268    }
269
270    /// Get the chains configuration map.
271    ///
272    /// Keys are CAIP-2 chain identifiers (e.g., "eip155:84532", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp").
273    pub fn chains(&self) -> &TChainsConfig {
274        &self.chains
275    }
276}
277
278impl<TChainsConfig> Config<TChainsConfig>
279where
280    TChainsConfig: Default + for<'de> Deserialize<'de>,
281{
282    /// Load configuration from CLI arguments and JSON file.
283    ///
284    /// The config file path is determined by:
285    /// 1. `--config <path>` CLI argument
286    /// 2. `./config.json` (if it exists)
287    ///
288    /// Values not present in the config file will be resolved via
289    /// environment variables or defaults during deserialization.
290    #[cfg(feature = "cli")]
291    pub fn load() -> Result<Self, ConfigError> {
292        let cli_args = CliArgs::parse();
293        let config_path = Path::new(&cli_args.config)
294            .canonicalize()
295            .map_err(|e| ConfigError::FileRead(cli_args.config, e))?;
296        Self::load_from_path(config_path)
297    }
298
299    /// Load configuration from a specific path (or use defaults if None).
300    pub fn load_from_path(path: PathBuf) -> Result<Self, ConfigError> {
301        let content = fs::read_to_string(&path).map_err(|e| ConfigError::FileRead(path, e))?;
302        let config: Config<TChainsConfig> = serde_json::from_str(&content)?;
303        Ok(config)
304    }
305}
306
307/// Configuration error types.
308#[derive(Debug, thiserror::Error)]
309pub enum ConfigError {
310    #[error("Failed to read config file at {0}: {1}")]
311    FileRead(PathBuf, std::io::Error),
312    #[error("Failed to parse config file: {0}")]
313    JsonParse(#[from] serde_json::Error),
314}