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}